forge-autonomy 2.7.35__tar.gz → 2.7.36__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.
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/PKG-INFO +1 -1
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/__main__.py +67 -0
- forge_autonomy-2.7.36/forge/preserved_lifecycle.py +458 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/qualification/server_runtime.py +78 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge_autonomy.egg-info/PKG-INFO +1 -1
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge_autonomy.egg-info/SOURCES.txt +2 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/product-version.json +1 -1
- forge_autonomy-2.7.36/tests/test_preserved_lifecycle.py +197 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/MANIFEST.in +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/README.md +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/_version.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/action_derivation_canary_closure.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/action_derivation_evidence.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/action_derivation_qualification.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/action_derivation_reattempt.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/ai_architect/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/ai_architect/provider_registry.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/api/installed-health-component-registry-1.0.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/api/operations-read-openapi-v1.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/api/operations-read-postman-v1.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/api/server-openapi-v1.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/api/server-postman-v1.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/architecture/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/architecture/advisor.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/architecture/presentation.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/architecture/workspace.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/autonomous_orchestrator.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/business/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/business/advisor.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/business/ingress.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/business/presentation.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/business/workspace.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/canary.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/capabilities/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/capabilities/delegation.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/completion/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/completion/assessor.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/completion/host_control_observer.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/completion/mission.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/completion/repository_observer.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/constitutional/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/constitutional/assessor.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/core/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/core/json_store.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/decision_evidence/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/decision_evidence/repository.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/dispatcher/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/dispatcher/dispatcher.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/ep_simulator.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/execution/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/execution/loop.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/execution/presentation.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/execution_host_configuration.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/foundation/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/foundation/loader.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/generation_two.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/generation_two_execution.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/governance/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/governance/execution_policy.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/governance/profiles.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/governance_authority.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/installed_health.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/installed_lifecycle.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/intake.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/integration/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/integration/coordinator.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/integration/repository.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/knowledge/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/knowledge/consumer.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/knowledge/registry.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/lifecycle/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/lifecycle/store.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/mission_amendment.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/mission_cli.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/mission_scheduler.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/action.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/action_derivation.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/agent_policy.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/ai_architect_provider.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/ai_architect_session.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/ai_provider_registry.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/architecture_mission.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/architecture_reasoning.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/architecture_review.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/codex_runtime_prompt.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/constitutional_validation.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/criterion_assessment.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/criterion_observation.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/decision_evidence.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/execution_host.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/foundation.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/historical_intent.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/integration.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/intent.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/intent_authoring.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/mission.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/mission_candidate.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/mission_completion.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/mission_planner.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/mission_recommendation.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/phase_completion.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/planning.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/producer.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/prompt_artifact.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/proposal.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/runtime_prompt.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/models/solution_template.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/operations_read_api.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/operator_identity.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/planner/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/planner/action_derivation.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/planner/codex_cli_session.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/planner/durable_derivation.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/planner/engine.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/planner/openai_responses.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/planner/provider_adapter.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/planning/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/planning/loader.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/planning/registry.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/portfolio_intelligence.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/programme_authorization.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/prompts/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/prompts/codex_cli.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/prompts/generator.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/proposals/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/proposals/generator.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/provider_context.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/provider_security.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/qualification/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/qualification/bootstrap_sequence.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/qualification/criterion_completion.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/qualification/generation_one.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/qualification/installed_smoke.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/recommendations/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/recommendations/engine.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/repository_truth.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/review/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/review/engine.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/runtime/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/runtime/bootstrap.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/runtime/data_root.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/runtime/database.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/runtime/dynamic_mission.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/runtime/evidence.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/runtime/execution_context.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/runtime/health.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/runtime/mission_controller.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/runtime/operational_reset.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/runtime/runner.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/runtime/service.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/scheduler/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/scheduler/adapter.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/scheduler/ep_http_adapter.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/scheduler/ep_v12.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/scheduler/scheduler.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/schemas/capability-registry-1.0.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/schemas/capability.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/schemas/common.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/schemas/decision-evidence-1.0.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/schemas/engineering-goal-0.5.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/schemas/engineering-increment-proposal-0.5.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/schemas/engineering-mode.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/schemas/engineering-plan-0.5.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/schemas/engineering-platform-peer-configuration-1.0.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/schemas/engineering-platform-peer-configuration-1.1.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/schemas/engineering-prompt-artifact-0.7.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/schemas/engineering-proposal-0.6.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/schemas/evidence-reference-0.5.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/schemas/foundation-document.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/schemas/governance-profile.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/schemas/knowledge-source-0.4.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/schemas/knowledge-source.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/schemas/mission-candidate-1.0.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/schemas/mission-candidate-1.1.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/schemas/phase-completion-1.0.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/schemas/planning-document-0.5.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/schemas/repository-catalog.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/schemas/repository.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/schemas/solution-template-1.0.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/schemas/workspace.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/secure_store.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/server_runtime.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/solutions/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/solutions/catalogue.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/solutions/generator.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/state/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/state/mission_state.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge/workspace/__init__.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge_autonomy.egg-info/dependency_links.txt +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge_autonomy.egg-info/entry_points.txt +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/forge_autonomy.egg-info/top_level.txt +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/pyproject.toml +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/schemas/capability-registry-1.0.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/schemas/capability.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/schemas/common.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/schemas/decision-evidence-1.0.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/schemas/engineering-goal-0.5.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/schemas/engineering-increment-proposal-0.5.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/schemas/engineering-mode.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/schemas/engineering-plan-0.5.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/schemas/engineering-prompt-artifact-0.7.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/schemas/engineering-proposal-0.6.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/schemas/evidence-reference-0.5.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/schemas/foundation-document.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/schemas/governance-profile.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/schemas/knowledge-source-0.4.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/schemas/knowledge-source.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/schemas/mission-candidate-1.0.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/schemas/mission-candidate-1.1.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/schemas/phase-completion-1.0.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/schemas/planning-document-0.5.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/schemas/repository-catalog.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/schemas/repository.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/schemas/solution-template-1.0.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/schemas/workspace.schema.json +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/setup.cfg +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_action_derivation.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_action_derivation_canary_closure.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_action_derivation_evidence.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_action_derivation_qualification.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_adaptive_action_sizing_design.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_agent_policy.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_aggregate_health_roadmap.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_ai_architect_provider.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_ai_architect_session.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_ai_development_projection.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_ai_provider_registry.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_architecture_reasoning.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_architecture_review_engine.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_architecture_workspace.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_autonomous_orchestrator.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_bootstrap_adapter.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_bootstrap_mission_canary.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_bootstrap_mission_runner.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_bootstrap_mission_scheduler.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_bootstrap_sequence_qualification.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_business_governance_ingress.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_business_workspace.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_canonical_data_root.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_capability_delegation.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_codex_cli_runtime_prompt.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_codex_cli_session.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_constitutional_validation.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_criterion_assessment_contract.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_criterion_ep_http_boundary.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_criterion_planning_context.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_decision_evidence.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_default_project_quality_policy.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_distribution_identity.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_durable_action_derivation.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_dynamic_mission_capability.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_engineering_action_architecture.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_engineering_intent_authoring.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_engineering_intent_lifecycle.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_engineering_mission.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_engineering_planning.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_engineering_prompt_artifacts.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_engineering_proposals.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_ep_http_adapter.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_ep_simulator.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_ep_simulator_rolling_planning.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_execution_context.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_execution_host_configuration.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_execution_host_contract.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_execution_loop.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_existing_release_reconciliation.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_forge_console_admin_parity_contract.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_forge_console_configuration_contract.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_forge_console_hosting_contract.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_forge_operations_console_roadmap.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_forge_outer_loop_ci_roadmap.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_forge_v1_bootstrap_scheduler_contract.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_forge_v1_implementation_dag.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_foundation_loader.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_foundation_models.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_generation_one_bootstrap_qualification.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_generation_two_execution.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_generation_two_mission_intake.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_governance_authority.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_governed_coverage.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_historical_engineering_intent.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_host_control_completion.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_inner_loop_ci_contract.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_installed_dynamic_mission_runtime.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_installed_forge_update.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_installed_lifecycle.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_integration_coordination.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_knowledge_consumption.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_live_project_roadmap_contract.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_mission_completion_evaluator.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_mission_controller.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_mission_dispatcher.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_mission_planner.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_mission_recommendation_engine.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_mission_recommendation_lifecycle.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_mission_runtime_projection.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_mission_runtime_scheduler.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_mission_state_store.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_mission_status.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_openai_action_derivation.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_operational_reset.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_operations_health.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_operations_read_api.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_operator_identity.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_phase_completion.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_platform_contract_documents.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_portfolio_intelligence_operation.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_producer_contract.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_product_version_operations.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_production_release_workflow.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_productization_reconciliation.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_programme_authorization.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_provider_context.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_provider_security.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_pypi_distribution_readback.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_recovered_partial_completion.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_release_operation.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_repository_truth.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_role_aware_conversations_contract.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_runtime_bootstrap.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_runtime_database.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_runtime_evidence.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_runtime_health.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_runtime_instance_persistence.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_runtime_prompt_generation.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_runtime_service.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_runtime_status_projection.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_scheduler_dependency_boundary.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_schemas.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_secure_store.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_server_api_contract.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_server_runtime.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_solution_templates.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_substantive_mission_completion.py +0 -0
- {forge_autonomy-2.7.35 → forge_autonomy-2.7.36}/tests/test_terminal_continuation_replay.py +0 -0
|
@@ -141,6 +141,28 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
141
141
|
uninstall_status.add_argument("--operation-id", required=True)
|
|
142
142
|
uninstall_status.add_argument("--instances-root", required=True)
|
|
143
143
|
uninstall_status.add_argument("--instance-id", required=True)
|
|
144
|
+
for lifecycle_name, lifecycle_help in (
|
|
145
|
+
("preserve", "preserve one exact quiescent Forge instance data root"),
|
|
146
|
+
("purge", "permanently purge one exact Forge instance"),
|
|
147
|
+
("restore", "validate one preserved Forge instance for same-identity restore"),
|
|
148
|
+
):
|
|
149
|
+
lifecycle = server_commands.add_parser(lifecycle_name, help=lifecycle_help)
|
|
150
|
+
lifecycle.add_argument("--operation-id", required=True)
|
|
151
|
+
lifecycle.add_argument("--instances-root", required=True)
|
|
152
|
+
lifecycle.add_argument("--instance-id", required=True)
|
|
153
|
+
lifecycle.add_argument("--runtime-id", required=True)
|
|
154
|
+
lifecycle.add_argument("--installation-id", required=True)
|
|
155
|
+
lifecycle.add_argument("--installed-version", required=True)
|
|
156
|
+
lifecycle.add_argument("--installed-source", required=True)
|
|
157
|
+
lifecycle.add_argument("--installed-artifact-digest", required=True)
|
|
158
|
+
if lifecycle_name == "restore":
|
|
159
|
+
lifecycle.add_argument("--preserve-operation-id", required=True)
|
|
160
|
+
lifecycle_status_parser = server_commands.add_parser(
|
|
161
|
+
"lifecycle-status", help="read one preserve/purge/restore lifecycle operation"
|
|
162
|
+
)
|
|
163
|
+
lifecycle_status_parser.add_argument("--operation-id", required=True)
|
|
164
|
+
lifecycle_status_parser.add_argument("--instances-root", required=True)
|
|
165
|
+
lifecycle_status_parser.add_argument("--instance-id", required=True)
|
|
144
166
|
reset = server_commands.add_parser("reset", help="operate the Forge-owned operational-history reset")
|
|
145
167
|
reset_commands = reset.add_subparsers(dest="reset_command", required=True)
|
|
146
168
|
reset_commands.add_parser("preview", help="inspect a read-only reset plan")
|
|
@@ -316,6 +338,51 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
316
338
|
return 0
|
|
317
339
|
except (InstalledLifecycleError, OSError, PermissionError, ValueError) as error:
|
|
318
340
|
return _failure("server uninstall-status", error)
|
|
341
|
+
if args.command == "server" and args.server_command in {"preserve", "purge", "restore"}:
|
|
342
|
+
if args.data_root is None:
|
|
343
|
+
return _failure(
|
|
344
|
+
"server " + args.server_command,
|
|
345
|
+
ValueError("--data-root is required for instance lifecycle mutation"),
|
|
346
|
+
)
|
|
347
|
+
try:
|
|
348
|
+
from .preserved_lifecycle import (
|
|
349
|
+
InstalledPreserveDispatcher,
|
|
350
|
+
InstalledPurgeDispatcher,
|
|
351
|
+
InstalledRestoreDispatcher,
|
|
352
|
+
InstanceLifecycleRequest,
|
|
353
|
+
RestoreRequest,
|
|
354
|
+
)
|
|
355
|
+
common = {
|
|
356
|
+
"operation_id": args.operation_id,
|
|
357
|
+
"instance_id": args.instance_id,
|
|
358
|
+
"runtime_id": args.runtime_id,
|
|
359
|
+
"installation_id": args.installation_id,
|
|
360
|
+
"installed_version": args.installed_version,
|
|
361
|
+
"installed_source": args.installed_source,
|
|
362
|
+
"installed_artifact_digest": args.installed_artifact_digest,
|
|
363
|
+
"data_root": args.data_root,
|
|
364
|
+
"instances_root": args.instances_root,
|
|
365
|
+
}
|
|
366
|
+
if args.server_command == "preserve":
|
|
367
|
+
result = InstalledPreserveDispatcher(InstanceLifecycleRequest(**common)).run()
|
|
368
|
+
elif args.server_command == "purge":
|
|
369
|
+
result = InstalledPurgeDispatcher(InstanceLifecycleRequest(**common)).run()
|
|
370
|
+
else:
|
|
371
|
+
result = InstalledRestoreDispatcher(RestoreRequest(
|
|
372
|
+
**common, preserve_operation_id=args.preserve_operation_id,
|
|
373
|
+
)).run()
|
|
374
|
+
print(json.dumps(result, sort_keys=True))
|
|
375
|
+
return 0
|
|
376
|
+
except (InstalledLifecycleError, OSError, sqlite3.Error, PermissionError, ValueError) as error:
|
|
377
|
+
return _failure("server " + args.server_command, error)
|
|
378
|
+
if args.command == "server" and args.server_command == "lifecycle-status":
|
|
379
|
+
try:
|
|
380
|
+
from .preserved_lifecycle import InstalledLifecycleError, lifecycle_status
|
|
381
|
+
result = lifecycle_status(args.instances_root, args.instance_id, args.operation_id)
|
|
382
|
+
print(json.dumps(result, sort_keys=True))
|
|
383
|
+
return 0
|
|
384
|
+
except (InstalledLifecycleError, OSError, PermissionError, ValueError) as error:
|
|
385
|
+
return _failure("server lifecycle-status", error)
|
|
319
386
|
if args.command == "health":
|
|
320
387
|
if args.data_root is None:
|
|
321
388
|
return _failure("health snapshot", ValueError("--data-root is required for health snapshot"))
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
"""Product-owned preserve/purge/restore semantics for Forge Server Runtime.
|
|
2
|
+
|
|
3
|
+
This contract extends, but never changes, the destructive
|
|
4
|
+
``forge-server-runtime-lifecycle/v1`` uninstall boundary. Deployment-owned
|
|
5
|
+
service definitions and immutable runtime slots remain outside Forge's mutable
|
|
6
|
+
instance-data ownership.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import asdict, dataclass
|
|
11
|
+
from hashlib import sha256
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import stat
|
|
15
|
+
from typing import Any, Callable, Mapping
|
|
16
|
+
|
|
17
|
+
from .installed_lifecycle import (
|
|
18
|
+
CONTROL_DIRECTORY,
|
|
19
|
+
InstalledLifecycleError,
|
|
20
|
+
InstalledUninstallDispatcher,
|
|
21
|
+
UninstallRequest,
|
|
22
|
+
_assert_identity,
|
|
23
|
+
_assert_no_symlink_components,
|
|
24
|
+
_assert_quiescent,
|
|
25
|
+
_atomic_json,
|
|
26
|
+
_digest_bytes,
|
|
27
|
+
_exclusive_locks,
|
|
28
|
+
_json_bytes,
|
|
29
|
+
_read_json,
|
|
30
|
+
_read_regular_bytes,
|
|
31
|
+
_runtime_snapshot,
|
|
32
|
+
_validate_identifier,
|
|
33
|
+
_DIGEST,
|
|
34
|
+
_SOURCE,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
INSTANCE_LIFECYCLE_CONTRACT = "forge-server-instance-lifecycle/v1"
|
|
39
|
+
LIFECYCLE_DIRECTORY = "instance-lifecycle-v1"
|
|
40
|
+
PROVIDER_AUTH_PRESERVED = "PRESERVED_REQUIRES_REVERIFICATION"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
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)
|
|
46
|
+
entries: list[dict[str, object]] = []
|
|
47
|
+
for directory, names, files in os.walk(root, topdown=True, followlinks=False):
|
|
48
|
+
parent = Path(directory)
|
|
49
|
+
for name in sorted(names):
|
|
50
|
+
path = parent / name
|
|
51
|
+
info = path.lstat()
|
|
52
|
+
if stat.S_ISLNK(info.st_mode) or not stat.S_ISDIR(info.st_mode):
|
|
53
|
+
raise InstalledLifecycleError(f"instance tree contains an unsafe directory entry: {path}")
|
|
54
|
+
entries.append({
|
|
55
|
+
"path": str(path.relative_to(root)),
|
|
56
|
+
"kind": "DIRECTORY",
|
|
57
|
+
"mode": stat.S_IMODE(info.st_mode),
|
|
58
|
+
})
|
|
59
|
+
for name in sorted(files):
|
|
60
|
+
path = parent / name
|
|
61
|
+
info = path.lstat()
|
|
62
|
+
if stat.S_ISLNK(info.st_mode) or not stat.S_ISREG(info.st_mode):
|
|
63
|
+
raise InstalledLifecycleError(f"instance tree contains an unsafe file entry: {path}")
|
|
64
|
+
payload = _read_regular_bytes(path)
|
|
65
|
+
entries.append({
|
|
66
|
+
"path": str(path.relative_to(root)),
|
|
67
|
+
"kind": "FILE",
|
|
68
|
+
"mode": stat.S_IMODE(info.st_mode),
|
|
69
|
+
"size": len(payload),
|
|
70
|
+
"digest": _digest_bytes(payload),
|
|
71
|
+
})
|
|
72
|
+
return _digest_bytes(_json_bytes(entries))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class InstanceLifecycleRequest:
|
|
77
|
+
operation_id: str
|
|
78
|
+
instance_id: str
|
|
79
|
+
runtime_id: str
|
|
80
|
+
installation_id: str
|
|
81
|
+
installed_version: str
|
|
82
|
+
installed_source: str
|
|
83
|
+
installed_artifact_digest: str
|
|
84
|
+
data_root: str
|
|
85
|
+
instances_root: str
|
|
86
|
+
|
|
87
|
+
def validate(self) -> None:
|
|
88
|
+
for label, value in (
|
|
89
|
+
("operation identity", self.operation_id),
|
|
90
|
+
("instance identity", self.instance_id),
|
|
91
|
+
("runtime identity", self.runtime_id),
|
|
92
|
+
("installation identity", self.installation_id),
|
|
93
|
+
):
|
|
94
|
+
_validate_identifier(label, value)
|
|
95
|
+
if self.instance_id != self.runtime_id:
|
|
96
|
+
raise InstalledLifecycleError("deployment instance does not bind the selected Forge runtime")
|
|
97
|
+
if _SOURCE.fullmatch(self.installed_source) is None or _DIGEST.fullmatch(self.installed_artifact_digest) is None:
|
|
98
|
+
raise InstalledLifecycleError("installed Forge artifact identity is invalid")
|
|
99
|
+
if not isinstance(self.installed_version, str) or not self.installed_version:
|
|
100
|
+
raise InstalledLifecycleError("installed Forge version is invalid")
|
|
101
|
+
data_root, instances_root = Path(self.data_root), Path(self.instances_root)
|
|
102
|
+
if not data_root.is_absolute() or not instances_root.is_absolute():
|
|
103
|
+
raise InstalledLifecycleError("lifecycle paths must be absolute")
|
|
104
|
+
_assert_no_symlink_components(instances_root)
|
|
105
|
+
_assert_no_symlink_components(data_root, allow_missing=True)
|
|
106
|
+
if data_root.parent != instances_root or data_root.name == CONTROL_DIRECTORY:
|
|
107
|
+
raise InstalledLifecycleError("data root is not one direct managed instance root")
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def digest(self) -> str:
|
|
111
|
+
return _digest_bytes(_json_bytes(asdict(self)))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(frozen=True)
|
|
115
|
+
class RestoreRequest(InstanceLifecycleRequest):
|
|
116
|
+
preserve_operation_id: str
|
|
117
|
+
|
|
118
|
+
def validate(self) -> None:
|
|
119
|
+
super().validate()
|
|
120
|
+
_validate_identifier("preserve operation identity", self.preserve_operation_id)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class _LifecycleOperation:
|
|
124
|
+
operation: str
|
|
125
|
+
|
|
126
|
+
def __init__(
|
|
127
|
+
self,
|
|
128
|
+
request: InstanceLifecycleRequest,
|
|
129
|
+
*,
|
|
130
|
+
interrupt_after: str | None = None,
|
|
131
|
+
clock: Callable[[], str] | None = None,
|
|
132
|
+
) -> None:
|
|
133
|
+
request.validate()
|
|
134
|
+
self.request = request
|
|
135
|
+
self.data_root = Path(request.data_root)
|
|
136
|
+
self.instances_root = Path(request.instances_root)
|
|
137
|
+
identity = sha256(request.instance_id.encode()).hexdigest()
|
|
138
|
+
self.control_root = self.instances_root / CONTROL_DIRECTORY / identity
|
|
139
|
+
self.lifecycle_root = self.control_root / LIFECYCLE_DIRECTORY
|
|
140
|
+
self.operation_root = self.lifecycle_root / "operations" / request.operation_id
|
|
141
|
+
self.state_path = self.operation_root / "state.json"
|
|
142
|
+
self.receipt_path = self.operation_root / "receipt.json"
|
|
143
|
+
self.purge_tombstone = self.lifecycle_root / "purged.json"
|
|
144
|
+
self.lifecycle_lock = self.control_root / "lifecycle.lock"
|
|
145
|
+
self.interrupt_after = interrupt_after
|
|
146
|
+
if clock is None:
|
|
147
|
+
from .installed_lifecycle import _now
|
|
148
|
+
clock = _now
|
|
149
|
+
self.clock = clock
|
|
150
|
+
|
|
151
|
+
def _interrupt(self, phase: str) -> None:
|
|
152
|
+
if self.interrupt_after == phase:
|
|
153
|
+
raise InterruptedError(f"interrupted after {phase}")
|
|
154
|
+
|
|
155
|
+
def _state(self) -> dict[str, Any]:
|
|
156
|
+
if self.state_path.exists():
|
|
157
|
+
state = _read_json(self.state_path)
|
|
158
|
+
if (
|
|
159
|
+
state.get("contract") != INSTANCE_LIFECYCLE_CONTRACT
|
|
160
|
+
or state.get("operation") != self.operation
|
|
161
|
+
or state.get("request") != asdict(self.request)
|
|
162
|
+
or state.get("request_digest") != self.request.digest
|
|
163
|
+
):
|
|
164
|
+
raise InstalledLifecycleError("operation identity already belongs to another lifecycle request")
|
|
165
|
+
return state
|
|
166
|
+
state = {
|
|
167
|
+
"contract": INSTANCE_LIFECYCLE_CONTRACT,
|
|
168
|
+
"operation": self.operation,
|
|
169
|
+
"request": asdict(self.request),
|
|
170
|
+
"request_digest": self.request.digest,
|
|
171
|
+
"phase": "PREPARED",
|
|
172
|
+
"prepared_at": self.clock(),
|
|
173
|
+
}
|
|
174
|
+
_atomic_json(self.state_path, state)
|
|
175
|
+
return state
|
|
176
|
+
|
|
177
|
+
def _save(self, state: Mapping[str, Any], phase: str, **evidence: object) -> dict[str, Any]:
|
|
178
|
+
updated = {**state, **evidence, "phase": phase, "updated_at": self.clock()}
|
|
179
|
+
_atomic_json(self.state_path, updated)
|
|
180
|
+
return updated
|
|
181
|
+
|
|
182
|
+
def _write_receipt(self, state: Mapping[str, Any], payload: Mapping[str, object]) -> dict[str, Any]:
|
|
183
|
+
receipt = {
|
|
184
|
+
"contract": INSTANCE_LIFECYCLE_CONTRACT,
|
|
185
|
+
"operation": self.operation,
|
|
186
|
+
"operation_id": self.request.operation_id,
|
|
187
|
+
"instance_id": self.request.instance_id,
|
|
188
|
+
"runtime_id": self.request.runtime_id,
|
|
189
|
+
"installation_id": self.request.installation_id,
|
|
190
|
+
"request_digest": self.request.digest,
|
|
191
|
+
"selected_artifact": {
|
|
192
|
+
"version": self.request.installed_version,
|
|
193
|
+
"source_revision": self.request.installed_source,
|
|
194
|
+
"artifact_digest": self.request.installed_artifact_digest,
|
|
195
|
+
},
|
|
196
|
+
"state": "COMPLETE",
|
|
197
|
+
**payload,
|
|
198
|
+
"completed_at": self.clock(),
|
|
199
|
+
}
|
|
200
|
+
receipt["receipt_digest"] = _digest_bytes(_json_bytes(receipt))
|
|
201
|
+
if self.receipt_path.exists():
|
|
202
|
+
existing = _read_json(self.receipt_path)
|
|
203
|
+
unsigned = {key: value for key, value in existing.items() if key != "receipt_digest"}
|
|
204
|
+
if (
|
|
205
|
+
existing.get("contract") != INSTANCE_LIFECYCLE_CONTRACT
|
|
206
|
+
or existing.get("operation") != self.operation
|
|
207
|
+
or existing.get("request_digest") != self.request.digest
|
|
208
|
+
or existing.get("receipt_digest") != _digest_bytes(_json_bytes(unsigned))
|
|
209
|
+
):
|
|
210
|
+
raise InstalledLifecycleError("terminal lifecycle receipt conflicts with existing evidence")
|
|
211
|
+
receipt = existing
|
|
212
|
+
else:
|
|
213
|
+
_atomic_json(self.receipt_path, receipt)
|
|
214
|
+
self._save(state, "COMPLETE", receipt_digest=receipt["receipt_digest"])
|
|
215
|
+
return receipt
|
|
216
|
+
|
|
217
|
+
def _terminal_receipt(self, state: Mapping[str, Any]) -> dict[str, Any]:
|
|
218
|
+
receipt = _read_json(self.receipt_path)
|
|
219
|
+
unsigned = {key: value for key, value in receipt.items() if key != "receipt_digest"}
|
|
220
|
+
if (
|
|
221
|
+
receipt.get("contract") != INSTANCE_LIFECYCLE_CONTRACT
|
|
222
|
+
or receipt.get("operation") != self.operation
|
|
223
|
+
or receipt.get("request_digest") != self.request.digest
|
|
224
|
+
or receipt.get("selected_artifact") != {
|
|
225
|
+
"version": self.request.installed_version,
|
|
226
|
+
"source_revision": self.request.installed_source,
|
|
227
|
+
"artifact_digest": self.request.installed_artifact_digest,
|
|
228
|
+
}
|
|
229
|
+
or receipt.get("state") != "COMPLETE"
|
|
230
|
+
or receipt.get("receipt_digest") != _digest_bytes(_json_bytes(unsigned))
|
|
231
|
+
or state.get("receipt_digest") != receipt.get("receipt_digest")
|
|
232
|
+
):
|
|
233
|
+
raise InstalledLifecycleError("terminal lifecycle receipt is inconsistent")
|
|
234
|
+
return receipt
|
|
235
|
+
|
|
236
|
+
def _runtime_locks(self) -> tuple[Path, ...]:
|
|
237
|
+
return (
|
|
238
|
+
self.data_root / "locks" / "forge-server-runtime.lock",
|
|
239
|
+
self.data_root / "forge-mission-controller.lock",
|
|
240
|
+
self.data_root / "forge-runtime-mutation.lock",
|
|
241
|
+
self.data_root / "locks" / "runtime.lock",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def _verified_snapshot(self) -> tuple[dict[str, Any], str]:
|
|
245
|
+
if not self.data_root.is_dir():
|
|
246
|
+
raise InstalledLifecycleError("selected preserved instance data is unavailable")
|
|
247
|
+
with _exclusive_locks(self._runtime_locks()):
|
|
248
|
+
snapshot = _runtime_snapshot(self.data_root)
|
|
249
|
+
_assert_identity(
|
|
250
|
+
snapshot,
|
|
251
|
+
runtime_id=self.request.runtime_id,
|
|
252
|
+
installation_id=self.request.installation_id,
|
|
253
|
+
installed_version=self.request.installed_version,
|
|
254
|
+
)
|
|
255
|
+
_assert_quiescent(snapshot)
|
|
256
|
+
tree_digest = _content_tree_digest(self.data_root)
|
|
257
|
+
return snapshot, tree_digest
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class InstalledPreserveDispatcher(_LifecycleOperation):
|
|
261
|
+
"""Durably attest one quiescent instance as data-preserved."""
|
|
262
|
+
|
|
263
|
+
operation = "PRESERVE"
|
|
264
|
+
|
|
265
|
+
def run(self) -> dict[str, Any]:
|
|
266
|
+
self.lifecycle_root.mkdir(parents=True, exist_ok=True)
|
|
267
|
+
os.chmod(self.lifecycle_root, 0o700)
|
|
268
|
+
with _exclusive_locks((self.lifecycle_lock,)):
|
|
269
|
+
if self.purge_tombstone.exists():
|
|
270
|
+
raise InstalledLifecycleError("instance identity was permanently purged")
|
|
271
|
+
state = self._state()
|
|
272
|
+
if state.get("phase") == "COMPLETE":
|
|
273
|
+
receipt = self._terminal_receipt(state)
|
|
274
|
+
_snapshot, current = self._verified_snapshot()
|
|
275
|
+
if current != receipt.get("mutable_instance_data_digest"):
|
|
276
|
+
raise InstalledLifecycleError("preserved instance data changed after terminal preserve")
|
|
277
|
+
return receipt
|
|
278
|
+
snapshot, tree_digest = self._verified_snapshot()
|
|
279
|
+
state = self._save(
|
|
280
|
+
state,
|
|
281
|
+
"VERIFIED",
|
|
282
|
+
runtime_snapshot_digest=snapshot["digest"],
|
|
283
|
+
mutable_instance_data_digest=tree_digest,
|
|
284
|
+
)
|
|
285
|
+
self._interrupt("verified")
|
|
286
|
+
return self._write_receipt(state, {
|
|
287
|
+
"lifecycle_state": "UNINSTALLED_DATA_PRESERVED",
|
|
288
|
+
"instance_identity": "PRESERVED",
|
|
289
|
+
"mutable_instance_data": "PRESERVED",
|
|
290
|
+
"mutable_instance_data_digest": tree_digest,
|
|
291
|
+
"restorable": True,
|
|
292
|
+
"service_state": "REMOVED_OR_INACTIVE",
|
|
293
|
+
"service_definition": "DEPLOYMENT_OWNER",
|
|
294
|
+
"immutable_runtime_slots": "PRESERVED",
|
|
295
|
+
"provider_auth_state": PROVIDER_AUTH_PRESERVED,
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class InstalledRestoreDispatcher(_LifecycleOperation):
|
|
300
|
+
"""Validate preserved Forge data for same-identity deployment-owner restore."""
|
|
301
|
+
|
|
302
|
+
operation = "RESTORE"
|
|
303
|
+
|
|
304
|
+
@property
|
|
305
|
+
def restore_request(self) -> RestoreRequest:
|
|
306
|
+
if not isinstance(self.request, RestoreRequest):
|
|
307
|
+
raise InstalledLifecycleError("restore request type is invalid")
|
|
308
|
+
return self.request
|
|
309
|
+
|
|
310
|
+
def _preserve_receipt(self) -> dict[str, Any]:
|
|
311
|
+
preserve_root = self.lifecycle_root / "operations" / self.restore_request.preserve_operation_id
|
|
312
|
+
receipt = _read_json(preserve_root / "receipt.json")
|
|
313
|
+
unsigned = {key: value for key, value in receipt.items() if key != "receipt_digest"}
|
|
314
|
+
if (
|
|
315
|
+
receipt.get("contract") != INSTANCE_LIFECYCLE_CONTRACT
|
|
316
|
+
or receipt.get("operation") != "PRESERVE"
|
|
317
|
+
or receipt.get("instance_id") != self.request.instance_id
|
|
318
|
+
or receipt.get("runtime_id") != self.request.runtime_id
|
|
319
|
+
or receipt.get("installation_id") != self.request.installation_id
|
|
320
|
+
or receipt.get("selected_artifact") != {
|
|
321
|
+
"version": self.request.installed_version,
|
|
322
|
+
"source_revision": self.request.installed_source,
|
|
323
|
+
"artifact_digest": self.request.installed_artifact_digest,
|
|
324
|
+
}
|
|
325
|
+
or receipt.get("lifecycle_state") != "UNINSTALLED_DATA_PRESERVED"
|
|
326
|
+
or receipt.get("restorable") is not True
|
|
327
|
+
or receipt.get("receipt_digest") != _digest_bytes(_json_bytes(unsigned))
|
|
328
|
+
):
|
|
329
|
+
raise InstalledLifecycleError("preserve evidence does not authorize same-identity restore")
|
|
330
|
+
return receipt
|
|
331
|
+
|
|
332
|
+
def run(self) -> dict[str, Any]:
|
|
333
|
+
self.lifecycle_root.mkdir(parents=True, exist_ok=True)
|
|
334
|
+
os.chmod(self.lifecycle_root, 0o700)
|
|
335
|
+
with _exclusive_locks((self.lifecycle_lock,)):
|
|
336
|
+
if self.purge_tombstone.exists():
|
|
337
|
+
raise InstalledLifecycleError("purged instance identity cannot be restored")
|
|
338
|
+
state = self._state()
|
|
339
|
+
if state.get("phase") == "COMPLETE":
|
|
340
|
+
return self._terminal_receipt(state)
|
|
341
|
+
preserve = self._preserve_receipt()
|
|
342
|
+
snapshot, tree_digest = self._verified_snapshot()
|
|
343
|
+
if tree_digest != preserve.get("mutable_instance_data_digest"):
|
|
344
|
+
raise InstalledLifecycleError("preserved instance data no longer matches restore evidence")
|
|
345
|
+
state = self._save(
|
|
346
|
+
state,
|
|
347
|
+
"VERIFIED",
|
|
348
|
+
runtime_snapshot_digest=snapshot["digest"],
|
|
349
|
+
preserve_operation_id=self.restore_request.preserve_operation_id,
|
|
350
|
+
preserve_receipt_digest=preserve["receipt_digest"],
|
|
351
|
+
mutable_instance_data_digest=tree_digest,
|
|
352
|
+
)
|
|
353
|
+
self._interrupt("verified")
|
|
354
|
+
return self._write_receipt(state, {
|
|
355
|
+
"lifecycle_state": "RESTORE_VALIDATED",
|
|
356
|
+
"instance_identity": "PRESERVED",
|
|
357
|
+
"mutable_instance_data": "PRESERVED",
|
|
358
|
+
"mutable_instance_data_digest": tree_digest,
|
|
359
|
+
"restored_from_preserve_operation": self.restore_request.preserve_operation_id,
|
|
360
|
+
"restored": True,
|
|
361
|
+
"service_state": "DEPLOYMENT_OWNER_REINSTALL_REQUIRED",
|
|
362
|
+
"service_definition": "DEPLOYMENT_OWNER",
|
|
363
|
+
"immutable_runtime_slots": "PRESERVED",
|
|
364
|
+
"provider_auth_state": PROVIDER_AUTH_PRESERVED,
|
|
365
|
+
"ready": False,
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
class InstalledPurgeDispatcher(_LifecycleOperation):
|
|
370
|
+
"""Explicit permanent purge projection over the legacy destructive uninstall."""
|
|
371
|
+
|
|
372
|
+
operation = "PURGE"
|
|
373
|
+
|
|
374
|
+
def run(self) -> dict[str, Any]:
|
|
375
|
+
self.lifecycle_root.mkdir(parents=True, exist_ok=True)
|
|
376
|
+
os.chmod(self.lifecycle_root, 0o700)
|
|
377
|
+
with _exclusive_locks((self.lifecycle_lock,)):
|
|
378
|
+
state = self._state()
|
|
379
|
+
if state.get("phase") == "COMPLETE":
|
|
380
|
+
receipt = self._terminal_receipt(state)
|
|
381
|
+
if self.data_root.exists() or self.data_root.is_symlink():
|
|
382
|
+
raise InstalledLifecycleError("purged instance target was recreated")
|
|
383
|
+
return receipt
|
|
384
|
+
|
|
385
|
+
uninstall = InstalledUninstallDispatcher(UninstallRequest(
|
|
386
|
+
operation_id=self.request.operation_id,
|
|
387
|
+
instance_id=self.request.instance_id,
|
|
388
|
+
runtime_id=self.request.runtime_id,
|
|
389
|
+
installation_id=self.request.installation_id,
|
|
390
|
+
data_root=self.request.data_root,
|
|
391
|
+
instances_root=self.request.instances_root,
|
|
392
|
+
)).run()
|
|
393
|
+
self._interrupt("uninstalled")
|
|
394
|
+
|
|
395
|
+
with _exclusive_locks((self.lifecycle_lock,)):
|
|
396
|
+
state = self._state()
|
|
397
|
+
if self.data_root.exists() or self.data_root.is_symlink():
|
|
398
|
+
raise InstalledLifecycleError("destructive uninstall did not remove selected instance data")
|
|
399
|
+
tombstone = {
|
|
400
|
+
"contract": INSTANCE_LIFECYCLE_CONTRACT,
|
|
401
|
+
"instance_id": self.request.instance_id,
|
|
402
|
+
"lifecycle_state": "PURGED",
|
|
403
|
+
"restorable": False,
|
|
404
|
+
"purge_operation_id": self.request.operation_id,
|
|
405
|
+
"uninstall_receipt_digest": uninstall["receipt_digest"],
|
|
406
|
+
}
|
|
407
|
+
tombstone["tombstone_digest"] = _digest_bytes(_json_bytes(tombstone))
|
|
408
|
+
if self.purge_tombstone.exists():
|
|
409
|
+
if _read_json(self.purge_tombstone) != tombstone:
|
|
410
|
+
raise InstalledLifecycleError("instance purge tombstone conflicts with prior evidence")
|
|
411
|
+
else:
|
|
412
|
+
_atomic_json(self.purge_tombstone, tombstone)
|
|
413
|
+
return self._write_receipt(state, {
|
|
414
|
+
"lifecycle_state": "PURGED",
|
|
415
|
+
"instance_identity": "RETIRED",
|
|
416
|
+
"mutable_instance_data": "REMOVED",
|
|
417
|
+
"restorable": False,
|
|
418
|
+
"service_state": "DEPLOYMENT_OWNER_REMOVAL_REQUIRED_OR_COMPLETE",
|
|
419
|
+
"service_definition": "DEPLOYMENT_OWNER",
|
|
420
|
+
"immutable_runtime_slots": "PRESERVED",
|
|
421
|
+
"provider_auth_state": "REMOVED_WITH_INSTANCE_DATA",
|
|
422
|
+
"legacy_uninstall_receipt_digest": uninstall["receipt_digest"],
|
|
423
|
+
"purge_tombstone_digest": tombstone["tombstone_digest"],
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def lifecycle_status(instances_root: str, instance_id: str, operation_id: str) -> dict[str, Any]:
|
|
428
|
+
"""Read one exact product-owned preserve/purge/restore operation."""
|
|
429
|
+
_validate_identifier("instance identity", instance_id)
|
|
430
|
+
_validate_identifier("operation identity", operation_id)
|
|
431
|
+
root = Path(instances_root)
|
|
432
|
+
if not root.is_absolute():
|
|
433
|
+
raise InstalledLifecycleError("instances root must be absolute")
|
|
434
|
+
_assert_no_symlink_components(root)
|
|
435
|
+
identity = sha256(instance_id.encode()).hexdigest()
|
|
436
|
+
operation_root = root / CONTROL_DIRECTORY / identity / LIFECYCLE_DIRECTORY / "operations" / operation_id
|
|
437
|
+
state = _read_json(operation_root / "state.json")
|
|
438
|
+
result = {
|
|
439
|
+
"contract": state.get("contract"),
|
|
440
|
+
"operation": state.get("operation"),
|
|
441
|
+
"operation_id": operation_id,
|
|
442
|
+
"instance_id": instance_id,
|
|
443
|
+
"phase": state.get("phase"),
|
|
444
|
+
"request_digest": state.get("request_digest"),
|
|
445
|
+
"state": "IN_PROGRESS",
|
|
446
|
+
}
|
|
447
|
+
if state.get("phase") == "COMPLETE":
|
|
448
|
+
receipt = _read_json(operation_root / "receipt.json")
|
|
449
|
+
unsigned = {key: value for key, value in receipt.items() if key != "receipt_digest"}
|
|
450
|
+
if receipt.get("receipt_digest") != _digest_bytes(_json_bytes(unsigned)):
|
|
451
|
+
raise InstalledLifecycleError("terminal lifecycle receipt is inconsistent")
|
|
452
|
+
result.update({
|
|
453
|
+
"state": "COMPLETE",
|
|
454
|
+
"lifecycle_state": receipt.get("lifecycle_state"),
|
|
455
|
+
"receipt_digest": receipt.get("receipt_digest"),
|
|
456
|
+
"restorable": receipt.get("restorable"),
|
|
457
|
+
})
|
|
458
|
+
return result
|
|
@@ -27,6 +27,13 @@ from forge.installed_lifecycle import (
|
|
|
27
27
|
UpdateAssessmentRequest,
|
|
28
28
|
assess_update,
|
|
29
29
|
)
|
|
30
|
+
from forge.preserved_lifecycle import (
|
|
31
|
+
InstalledPreserveDispatcher,
|
|
32
|
+
InstalledPurgeDispatcher,
|
|
33
|
+
InstalledRestoreDispatcher,
|
|
34
|
+
InstanceLifecycleRequest,
|
|
35
|
+
RestoreRequest,
|
|
36
|
+
)
|
|
30
37
|
from forge.models import (
|
|
31
38
|
ExecutionEvidenceOutcome,
|
|
32
39
|
ExecutionRequest,
|
|
@@ -239,6 +246,9 @@ def run(*, candidate_wheel: Path | None = None, source_revision: str | None = No
|
|
|
239
246
|
"update_assessment": "NOT_REQUESTED",
|
|
240
247
|
"uninstall_dispatcher": "PASS",
|
|
241
248
|
"uninstall_replay": "PASS",
|
|
249
|
+
"preserve": "NOT_REQUESTED",
|
|
250
|
+
"restore": "NOT_REQUESTED",
|
|
251
|
+
"purge": "NOT_REQUESTED",
|
|
242
252
|
}
|
|
243
253
|
if candidate_wheel is not None:
|
|
244
254
|
if source_revision is None or len(source_revision) != 40:
|
|
@@ -254,6 +264,71 @@ def run(*, candidate_wheel: Path | None = None, source_revision: str | None = No
|
|
|
254
264
|
if assessment.get("state") != "UP_TO_DATE" or assessment.get("mutating") is not False:
|
|
255
265
|
raise RuntimeError("installed lifecycle update assessment did not bind the exact artifact")
|
|
256
266
|
lifecycle["update_assessment"] = "PASS"
|
|
267
|
+
|
|
268
|
+
preserve_request = InstanceLifecycleRequest(
|
|
269
|
+
operation_id="forge-server-runtime-installed-preserve",
|
|
270
|
+
instance_id=identities[0],
|
|
271
|
+
runtime_id=identities[0],
|
|
272
|
+
installation_id=installation_ids[0],
|
|
273
|
+
installed_version=version,
|
|
274
|
+
installed_source=source_revision,
|
|
275
|
+
installed_artifact_digest=wheel_digest,
|
|
276
|
+
data_root=str(roots[0]),
|
|
277
|
+
instances_root=str(root.resolve()),
|
|
278
|
+
)
|
|
279
|
+
preserve = InstalledPreserveDispatcher(preserve_request).run()
|
|
280
|
+
if (
|
|
281
|
+
preserve.get("lifecycle_state") != "UNINSTALLED_DATA_PRESERVED"
|
|
282
|
+
or preserve.get("instance_identity") != "PRESERVED"
|
|
283
|
+
or preserve.get("mutable_instance_data") != "PRESERVED"
|
|
284
|
+
or preserve.get("restorable") is not True
|
|
285
|
+
or preserve.get("provider_auth_state") != "PRESERVED_REQUIRES_REVERIFICATION"
|
|
286
|
+
or not roots[0].is_dir()
|
|
287
|
+
):
|
|
288
|
+
raise RuntimeError("installed lifecycle preserve semantics are unavailable")
|
|
289
|
+
lifecycle["preserve"] = "PASS"
|
|
290
|
+
|
|
291
|
+
restore_request = RestoreRequest(
|
|
292
|
+
operation_id="forge-server-runtime-installed-restore",
|
|
293
|
+
instance_id=identities[0],
|
|
294
|
+
runtime_id=identities[0],
|
|
295
|
+
installation_id=installation_ids[0],
|
|
296
|
+
installed_version=version,
|
|
297
|
+
installed_source=source_revision,
|
|
298
|
+
installed_artifact_digest=wheel_digest,
|
|
299
|
+
data_root=str(roots[0]),
|
|
300
|
+
instances_root=str(root.resolve()),
|
|
301
|
+
preserve_operation_id="forge-server-runtime-installed-preserve",
|
|
302
|
+
)
|
|
303
|
+
restored = InstalledRestoreDispatcher(restore_request).run()
|
|
304
|
+
if (
|
|
305
|
+
restored.get("lifecycle_state") != "RESTORE_VALIDATED"
|
|
306
|
+
or restored.get("instance_identity") != "PRESERVED"
|
|
307
|
+
or restored.get("provider_auth_state") != "PRESERVED_REQUIRES_REVERIFICATION"
|
|
308
|
+
or restored.get("ready") is not False
|
|
309
|
+
):
|
|
310
|
+
raise RuntimeError("installed lifecycle restore semantics are unavailable")
|
|
311
|
+
lifecycle["restore"] = "PASS"
|
|
312
|
+
|
|
313
|
+
purge_request = InstanceLifecycleRequest(
|
|
314
|
+
operation_id="forge-server-runtime-installed-purge",
|
|
315
|
+
instance_id=identities[0],
|
|
316
|
+
runtime_id=identities[0],
|
|
317
|
+
installation_id=installation_ids[0],
|
|
318
|
+
installed_version=version,
|
|
319
|
+
installed_source=source_revision,
|
|
320
|
+
installed_artifact_digest=wheel_digest,
|
|
321
|
+
data_root=str(roots[0]),
|
|
322
|
+
instances_root=str(root.resolve()),
|
|
323
|
+
)
|
|
324
|
+
purged = InstalledPurgeDispatcher(purge_request).run()
|
|
325
|
+
if (
|
|
326
|
+
purged.get("lifecycle_state") != "PURGED"
|
|
327
|
+
or purged.get("restorable") is not False
|
|
328
|
+
or roots[0].exists()
|
|
329
|
+
):
|
|
330
|
+
raise RuntimeError("installed lifecycle purge semantics are unavailable")
|
|
331
|
+
lifecycle["purge"] = "PASS"
|
|
257
332
|
uninstall_request = UninstallRequest(
|
|
258
333
|
operation_id="forge-server-runtime-installed-uninstall",
|
|
259
334
|
instance_id=identities[1], runtime_id=identities[1], installation_id=installation_ids[1],
|
|
@@ -321,6 +396,9 @@ def run(*, candidate_wheel: Path | None = None, source_revision: str | None = No
|
|
|
321
396
|
"lifecycle_update_assessment": lifecycle["update_assessment"],
|
|
322
397
|
"lifecycle_uninstall_dispatcher": lifecycle["uninstall_dispatcher"],
|
|
323
398
|
"lifecycle_uninstall_replay": lifecycle["uninstall_replay"],
|
|
399
|
+
"lifecycle_preserve": lifecycle["preserve"],
|
|
400
|
+
"lifecycle_restore": lifecycle["restore"],
|
|
401
|
+
"lifecycle_purge": lifecycle["purge"],
|
|
324
402
|
}
|
|
325
403
|
|
|
326
404
|
|
|
@@ -25,6 +25,7 @@ forge/mission_scheduler.py
|
|
|
25
25
|
forge/operations_read_api.py
|
|
26
26
|
forge/operator_identity.py
|
|
27
27
|
forge/portfolio_intelligence.py
|
|
28
|
+
forge/preserved_lifecycle.py
|
|
28
29
|
forge/programme_authorization.py
|
|
29
30
|
forge/provider_context.py
|
|
30
31
|
forge/provider_security.py
|
|
@@ -303,6 +304,7 @@ tests/test_operator_identity.py
|
|
|
303
304
|
tests/test_phase_completion.py
|
|
304
305
|
tests/test_platform_contract_documents.py
|
|
305
306
|
tests/test_portfolio_intelligence_operation.py
|
|
307
|
+
tests/test_preserved_lifecycle.py
|
|
306
308
|
tests/test_producer_contract.py
|
|
307
309
|
tests/test_product_version_operations.py
|
|
308
310
|
tests/test_production_release_workflow.py
|