engineering-platform 2.3.88__tar.gz → 2.3.89__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.
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/PKG-INFO +1 -1
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/pyproject.toml +1 -1
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/ENGINEERING_PLATFORM_CONFIG.json +1 -1
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/ENGINEERING_PLATFORM_VERSION.json +4 -4
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/central_operational_reset.py +2 -1
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/execution_host.py +37 -13
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/execution_repository.py +138 -18
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/merge_delegation.py +127 -13
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/platform_version.py +1 -1
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/server.py +103 -5
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/submission_service.py +2 -1
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/templates/workspace-config.json +1 -1
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform.egg-info/PKG-INFO +1 -1
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/README.md +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/setup.cfg +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/__init__.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/__main__.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/agent_state.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/agent_trust.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/assets/dashboard.css +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/assets/dashboard.js +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/assets/dashboard_locales.mjs +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/assets/dashboard_status_store.mjs +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/assets/dashboard_translation.mjs +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/assets/operations-console/apple-touch-icon-dark.png +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/assets/operations-console/apple-touch-icon-light.png +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/assets/operations-console/icon-dark.png +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/assets/operations-console/icon-light.png +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/assets/operations-console/icon-transparent.png +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/assets/operations-console/manifest.webmanifest +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/assurance_scope.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/capability_preflight.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/capability_review.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/central_data_transfer.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/central_database.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/central_store_migration.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/codex_capacity.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/codex_chat.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/codex_observability.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/component_lock.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/component_logging.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/console_presentation.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/console_route_ownership.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/contracts/__init__.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/contracts/ep_consumer.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/contracts/models.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/contracts/projection.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/dashboard_browser_validation.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/dashboard_state.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/dashboard_supervisor.swift +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/dashboard_translation.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/dependabot_producer.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/development_profile.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/drift_diagnostics.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/emergency_recovery.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/engineering_memory.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/ep_consumer_credentials.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/evidence_projection.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/execution_activity.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/execution_context.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/execution_errors.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/execution_evidence.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/execution_executor.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/execution_finalization.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/execution_host_evidence.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/execution_lease.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/execution_lifecycle.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/execution_models.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/execution_readiness.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/execution_reporting.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/execution_timeout_policy.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/execution_timing.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/execution_transaction.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/external_producer_binding.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/file_inbox.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/forensic_attribution.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/forensic_attribution_v2.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/forensic_delta.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/golden_scenario.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/historical_dashboard_configuration.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/host_admin.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/host_preflight.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/installation_relocation.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/installation_update_activation.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/installation_update_admission.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/installation_update_backup.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/installation_update_composition.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/installation_update_executor.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/installation_update_migration.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/installation_update_operation.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/installation_update_plan.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/installation_update_preparation.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/investigation_ledger.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/legacy_inbox_migration.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/legacy_installation_adoption.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/lifecycle_worker.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/live_status.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/local_api.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/local_api_keychain.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/local_repository_binding.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/managed_autonomy.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/managed_codex_runtime.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/operational_installation.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/operational_installation_lock.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/operational_installation_record.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/owner_credential_recovery.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/parity_context.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/parity_lifecycle_dispatcher.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/platform_admin.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/platform_api.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/platform_bootstrap.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/platform_components.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/pr_check_repair.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/pr_evidence_backfill.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/producer.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/product_installation_readback.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/project_agent.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/project_agent_service.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/project_topology.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/prompt_history.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/provider_context.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/provider_context_benchmark.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/provider_context_scope.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/provider_interruption.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/provider_process_identity.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/provider_readiness.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/provider_recovery.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/provider_usage.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/providers.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/qualification.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/qualification_runtime.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/recommendation_handoff.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/reconciliation_adoption.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/release_operation.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/report_analysis.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/repository_attachment.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/repository_handoff.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/resources.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/reviewer_evidence.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/revision_binding.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/schemas/producer-readback-v1.2.schema.json +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/schemas/producer-readback-v1.schema.json +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/schemas/repository-attachment.schema.json +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/server_console_services.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/server_relay.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/server_service.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/status_model.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/status_reconciliation.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/storage.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/submission_cli.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/submission_intake.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/system_installation_topology.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/system_server_service.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/telemetry.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/telemetry_contract.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/telemetry_export.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/telemetry_metrics.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/terminal_evidence_reconciliation.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/validation_identity.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/validation_profile.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/version_preparation_delivery.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/workspace_preflight.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/worktree_provenance.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform/worktree_tooling.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform.egg-info/SOURCES.txt +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform.egg-info/dependency_links.txt +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform.egg-info/entry_points.txt +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/src/engineering_platform.egg-info/top_level.txt +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/tests/test_adaptive_action_sizing_design.py +0 -0
- {engineering_platform-2.3.88 → engineering_platform-2.3.89}/tests/test_role_task_model_policy_design.py +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bootstrap_contract": "2026.12",
|
|
3
3
|
"checkpoint_format": 1,
|
|
4
|
-
"dashboard_version": "2.3.
|
|
4
|
+
"dashboard_version": "2.3.89",
|
|
5
5
|
"handoff_protocol": 1,
|
|
6
6
|
"memory_format": 2,
|
|
7
7
|
"minimum_codex_cli": "0.146.0",
|
|
8
8
|
"inbox_protocol": 1,
|
|
9
|
-
"platform_version": "2.3.
|
|
9
|
+
"platform_version": "2.3.89",
|
|
10
10
|
"report_format": 2,
|
|
11
|
-
"runner_version": "2.3.
|
|
11
|
+
"runner_version": "2.3.89",
|
|
12
12
|
"status_model": 1,
|
|
13
13
|
"storage_schema": 45,
|
|
14
|
-
"watcher_version": "2.3.
|
|
14
|
+
"watcher_version": "2.3.89"
|
|
15
15
|
}
|
|
@@ -36,7 +36,7 @@ from .storage import sqlite_connection
|
|
|
36
36
|
|
|
37
37
|
PROFILE = "EP_CENTRAL_OPERATIONAL_HISTORY_V1"
|
|
38
38
|
PLAN_VERSION = 2
|
|
39
|
-
SCHEMA_VERSION =
|
|
39
|
+
SCHEMA_VERSION = 72
|
|
40
40
|
_OPERATION = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{7,127}")
|
|
41
41
|
_INSTANCE_ID = re.compile(
|
|
42
42
|
r"[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}"
|
|
@@ -66,6 +66,7 @@ INSTALLATION_AND_CONFIGURATION = frozenset({
|
|
|
66
66
|
"ep_project_registrations", "ep_repository_registrations",
|
|
67
67
|
"ep_agent_registrations", "ep_agent_repository_attachments",
|
|
68
68
|
"ep_local_repository_bindings", "ep_external_producer_bindings",
|
|
69
|
+
"ep_assurance_target_selections",
|
|
69
70
|
"execution_migration_provenance",
|
|
70
71
|
})
|
|
71
72
|
SECURITY_AND_AUTHORITY_LEDGER = frozenset({
|
|
@@ -3898,12 +3898,14 @@ First implementation pull-request publication gate:
|
|
|
3898
3898
|
try:
|
|
3899
3899
|
with sqlite_connection(self.store.central_database) as connection:
|
|
3900
3900
|
grant = merge_delegation.load(connection, state.merge_delegation_id)
|
|
3901
|
+
selection_current = bool(grant and merge_delegation.target_selection_permits(connection, grant))
|
|
3901
3902
|
except (EngineeringStorageError, sqlite3.Error):
|
|
3902
3903
|
return False
|
|
3903
|
-
return bool(grant and grant.
|
|
3904
|
-
and grant.assurance_profile_revision == "1"
|
|
3904
|
+
return bool(grant and selection_current and grant.assurance_profile_revision == "1"
|
|
3905
3905
|
and grant.github_repository == state.repository
|
|
3906
|
-
== merge_delegation.
|
|
3906
|
+
and (grant.assurance_profile_id == merge_delegation.REPOSITORY_ASSURANCE_ID
|
|
3907
|
+
or (grant.assurance_profile_id == "qualification-autonomous-qs"
|
|
3908
|
+
and state.repository == merge_delegation.AUTONOMOUS_ASSURANCE_REPOSITORY)))
|
|
3907
3909
|
|
|
3908
3910
|
def _recover_autonomous_pr_assurance(
|
|
3909
3911
|
self, state: TransactionState, candidate: PullRequestEvidence,
|
|
@@ -4013,7 +4015,8 @@ First implementation pull-request publication gate:
|
|
|
4013
4015
|
(state.run_id,),
|
|
4014
4016
|
).fetchone()
|
|
4015
4017
|
grant = merge_delegation.load(connection, state.merge_delegation_id)
|
|
4016
|
-
|
|
4018
|
+
selection_current = bool(grant and merge_delegation.target_selection_permits(connection, grant))
|
|
4019
|
+
if scope is None or grant is None or not selection_current or not isinstance(accepted, dict):
|
|
4017
4020
|
return None
|
|
4018
4021
|
if Path(str(scope[2])).resolve() != self.root.resolve():
|
|
4019
4022
|
return None
|
|
@@ -4057,17 +4060,21 @@ First implementation pull-request publication gate:
|
|
|
4057
4060
|
qualifier = getattr(self.github, "delegated_merge_qualification", None)
|
|
4058
4061
|
if not callable(qualifier):
|
|
4059
4062
|
return None
|
|
4060
|
-
autonomous_profile = (
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4063
|
+
autonomous_profile = (grant.assurance_profile_revision == "1" and
|
|
4064
|
+
grant.assurance_profile_id in {
|
|
4065
|
+
"qualification-autonomous-qs",
|
|
4066
|
+
merge_delegation.REPOSITORY_ASSURANCE_ID})
|
|
4064
4067
|
if grant.assurance_profile_id or grant.assurance_profile_revision:
|
|
4065
4068
|
if (not autonomous_profile
|
|
4066
|
-
or grant.
|
|
4069
|
+
or (grant.assurance_profile_id == "qualification-autonomous-qs"
|
|
4070
|
+
and grant.github_repository != merge_delegation.AUTONOMOUS_ASSURANCE_REPOSITORY)
|
|
4067
4071
|
or not self._autonomous_merge_assurance_passes(state, expected_head)):
|
|
4068
4072
|
return None
|
|
4073
|
+
selected_profile = (merge_delegation.REPOSITORY_ASSURANCE_PROFILE
|
|
4074
|
+
if grant.assurance_profile_id == merge_delegation.REPOSITORY_ASSURANCE_ID
|
|
4075
|
+
else merge_delegation.AUTONOMOUS_ASSURANCE_PROFILE)
|
|
4069
4076
|
qualification = (qualifier(fresh.number, expected_head,
|
|
4070
|
-
assurance_profile=
|
|
4077
|
+
assurance_profile=selected_profile)
|
|
4071
4078
|
if autonomous_profile else qualifier(fresh.number, expected_head))
|
|
4072
4079
|
if (not isinstance(qualification, dict) or qualification.get("conclusion") != "PASS"
|
|
4073
4080
|
or qualification.get("exact_qualified_sha") != expected_head
|
|
@@ -4094,7 +4101,8 @@ First implementation pull-request publication gate:
|
|
|
4094
4101
|
return None
|
|
4095
4102
|
with sqlite_connection(self.store.central_database) as connection:
|
|
4096
4103
|
current_grant = merge_delegation.load(connection, state.merge_delegation_id)
|
|
4097
|
-
|
|
4104
|
+
current_selection = bool(current_grant and merge_delegation.target_selection_permits(connection, current_grant))
|
|
4105
|
+
if (current_grant is None or not current_selection or not current_grant.permits(
|
|
4098
4106
|
project_id=str(scope[0]), repository_id=str(scope[1]),
|
|
4099
4107
|
mission_id=str(accepted.get("mission_id")), mission_revision=str(mission_revision),
|
|
4100
4108
|
role=state.transaction_kind, base_branch="main",
|
|
@@ -4108,7 +4116,9 @@ First implementation pull-request publication gate:
|
|
|
4108
4116
|
policy_reader = getattr(self.github, "_autonomous_effective_policy", None)
|
|
4109
4117
|
if not callable(policy_reader):
|
|
4110
4118
|
return None
|
|
4111
|
-
current_policy = policy_reader()
|
|
4119
|
+
current_policy = (policy_reader(assurance_profile=selected_profile)
|
|
4120
|
+
if grant.assurance_profile_id == merge_delegation.REPOSITORY_ASSURANCE_ID
|
|
4121
|
+
else policy_reader())
|
|
4112
4122
|
if (not isinstance(current_policy, dict)
|
|
4113
4123
|
or current_policy.get("digest") != current_grant.assurance_policy_digest):
|
|
4114
4124
|
return None
|
|
@@ -4118,7 +4128,21 @@ First implementation pull-request publication gate:
|
|
|
4118
4128
|
attempted = replace(state, delegated_merge_attempt=attempt,
|
|
4119
4129
|
delegated_merge_actor_reference=None)
|
|
4120
4130
|
self.store.save(attempted)
|
|
4121
|
-
|
|
4131
|
+
# Linearize revocation against the external merge call. A revoke
|
|
4132
|
+
# committed before this lock is observed; one waiting for this lock
|
|
4133
|
+
# can only commit after the bounded call has returned.
|
|
4134
|
+
with sqlite_connection(self.store.central_database) as authority:
|
|
4135
|
+
authority.execute("BEGIN IMMEDIATE")
|
|
4136
|
+
final_grant = merge_delegation.load(authority, state.merge_delegation_id)
|
|
4137
|
+
if (final_grant is None or not final_grant.permits(
|
|
4138
|
+
project_id=str(scope[0]), repository_id=str(scope[1]),
|
|
4139
|
+
mission_id=str(accepted.get("mission_id")),
|
|
4140
|
+
mission_revision=str(mission_revision),
|
|
4141
|
+
role=state.transaction_kind, base_branch="main")
|
|
4142
|
+
or not merge_delegation.target_selection_permits(authority, final_grant)
|
|
4143
|
+
or final_grant != current_grant):
|
|
4144
|
+
raise ValueError("merge authority changed before the external call")
|
|
4145
|
+
self.github.merge(fresh.number, expected_head_sha=expected_head)
|
|
4122
4146
|
merged = self.github.pull_request(fresh.number)
|
|
4123
4147
|
self.repository.refresh_main_reference(self.root)
|
|
4124
4148
|
if (merged.state == "MERGED" and merged.head_sha == expected_head
|
|
@@ -360,10 +360,17 @@ class GhCliClient:
|
|
|
360
360
|
except RuntimeError as error:
|
|
361
361
|
raise RunnerError("Pull request Markdown could not be normalized.") from error
|
|
362
362
|
return True
|
|
363
|
-
def _autonomous_effective_policy(self) -> dict[str, object]:
|
|
363
|
+
def _autonomous_effective_policy(self, *, assurance_profile: str = "qualification-autonomous-qs@1") -> dict[str, object]:
|
|
364
364
|
"""Read all active main rules and classic protection without inferring zero from errors."""
|
|
365
|
-
if
|
|
365
|
+
if (assurance_profile == "qualification-autonomous-qs@1"
|
|
366
|
+
and self.repository != "pcvantol/forge-mission-qualification"):
|
|
366
367
|
raise RunnerError("Autonomous assurance profile has a different repository scope.")
|
|
368
|
+
if assurance_profile not in {"qualification-autonomous-qs@1", "repository-autonomous-qs@1"}:
|
|
369
|
+
raise RunnerError("Unknown delegated merge assurance profile.")
|
|
370
|
+
repository_profile = assurance_profile == "repository-autonomous-qs@1"
|
|
371
|
+
if repository_profile and (not isinstance(self.repository, str)
|
|
372
|
+
or re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", self.repository) is None):
|
|
373
|
+
raise RunnerError("Autonomous assurance requires an exact repository.")
|
|
367
374
|
try:
|
|
368
375
|
raw_rules = json.loads(self._github("api", f"repos/{self.repository}/rules/branches/main?per_page=100"))
|
|
369
376
|
except (RuntimeError, json.JSONDecodeError) as error:
|
|
@@ -415,6 +422,13 @@ class GhCliClient:
|
|
|
415
422
|
required_apps: dict[str, set[int]] = {}
|
|
416
423
|
strict_values: list[bool] = []
|
|
417
424
|
required_approvals: list[int] = []
|
|
425
|
+
required_thread_resolution = False
|
|
426
|
+
classic_conversations = classic.get("required_conversation_resolution") if classic else None
|
|
427
|
+
if repository_profile and classic_conversations is not None:
|
|
428
|
+
if (not isinstance(classic_conversations, dict)
|
|
429
|
+
or not isinstance(classic_conversations.get("enabled"), bool)):
|
|
430
|
+
raise RunnerError("Classic review-thread policy is incomplete.")
|
|
431
|
+
required_thread_resolution = classic_conversations["enabled"]
|
|
418
432
|
classic_checks = classic.get("required_status_checks") if classic else None
|
|
419
433
|
if classic_checks is not None:
|
|
420
434
|
if not isinstance(classic_checks, dict) or not isinstance(classic_checks.get("strict"), bool):
|
|
@@ -442,9 +456,10 @@ class GhCliClient:
|
|
|
442
456
|
if (isinstance(count, bool) or not isinstance(count, int) or count < 0
|
|
443
457
|
or classic_reviews.get("require_code_owner_reviews") is True
|
|
444
458
|
or classic_reviews.get("require_last_push_approval") is True
|
|
445
|
-
or classic_reviews.get("required_review_thread_resolution") is True):
|
|
459
|
+
or (classic_reviews.get("required_review_thread_resolution") is True and not repository_profile)):
|
|
446
460
|
raise RunnerError("Classic review policy requires unsupported or unknown approval evidence.")
|
|
447
461
|
required_approvals.append(count)
|
|
462
|
+
required_thread_resolution |= classic_reviews.get("required_review_thread_resolution") is True
|
|
448
463
|
supported = {"required_status_checks", "pull_request", "non_fast_forward", "deletion", "required_linear_history"}
|
|
449
464
|
for rule in verified_rules:
|
|
450
465
|
rule_type, parameters = rule.get("type"), rule.get("parameters")
|
|
@@ -467,13 +482,28 @@ class GhCliClient:
|
|
|
467
482
|
elif rule_type == "pull_request":
|
|
468
483
|
if not isinstance(parameters, dict):
|
|
469
484
|
raise RunnerError("Ruleset pull-request policy is incomplete.")
|
|
485
|
+
if repository_profile:
|
|
486
|
+
supported_parameters = {
|
|
487
|
+
"allowed_merge_methods", "dismiss_stale_reviews_on_push",
|
|
488
|
+
"require_code_owner_review", "require_extra_approval_for_unattributed_changes",
|
|
489
|
+
"require_last_push_approval", "required_approving_review_count",
|
|
490
|
+
"required_review_thread_resolution", "required_reviewers",
|
|
491
|
+
}
|
|
492
|
+
if (set(parameters) - supported_parameters
|
|
493
|
+
or parameters.get("require_extra_approval_for_unattributed_changes") is True
|
|
494
|
+
or parameters.get("require_last_push_approval") is True
|
|
495
|
+
or ("allowed_merge_methods" in parameters
|
|
496
|
+
and (not isinstance(parameters["allowed_merge_methods"], list)
|
|
497
|
+
or "squash" not in parameters["allowed_merge_methods"]))):
|
|
498
|
+
raise RunnerError("Ruleset pull-request policy has an unsupported merge condition.")
|
|
470
499
|
count = parameters.get("required_approving_review_count")
|
|
471
500
|
if (isinstance(count, bool) or not isinstance(count, int) or count < 0
|
|
472
501
|
or parameters.get("require_code_owner_review") is True
|
|
473
|
-
or parameters.get("required_review_thread_resolution") is True
|
|
502
|
+
or (parameters.get("required_review_thread_resolution") is True and not repository_profile)
|
|
474
503
|
or parameters.get("required_reviewers")):
|
|
475
504
|
raise RunnerError("Ruleset review policy requires unsupported or unknown approval evidence.")
|
|
476
505
|
required_approvals.append(count)
|
|
506
|
+
required_thread_resolution |= parameters.get("required_review_thread_resolution") is True
|
|
477
507
|
if not required_checks or not strict_values or not all(strict_values):
|
|
478
508
|
raise RunnerError("Autonomous assurance requires strict protected status checks.")
|
|
479
509
|
effective_rule_types = {rule.get("type") for rule in verified_rules}
|
|
@@ -484,16 +514,74 @@ class GhCliClient:
|
|
|
484
514
|
policy = {"source": "github-effective-main", "classic": classic,
|
|
485
515
|
"rulesets": verified_rules, "required_checks": sorted(required_checks),
|
|
486
516
|
"required_apps": {name: sorted(apps) for name, apps in sorted(required_apps.items())},
|
|
487
|
-
"required_approvals": max(required_approvals, default=0), "strict_checks": True
|
|
488
|
-
|
|
517
|
+
"required_approvals": max(required_approvals, default=0), "strict_checks": True,
|
|
518
|
+
"required_thread_resolution": required_thread_resolution}
|
|
519
|
+
if repository_profile:
|
|
520
|
+
# GitHub embeds observation URLs and other changing metadata in
|
|
521
|
+
# protection responses. Bind only the effective rule semantics.
|
|
522
|
+
classic_binding = None
|
|
523
|
+
if classic is not None:
|
|
524
|
+
classic_binding = {
|
|
525
|
+
"enforce_admins": classic["enforce_admins"]["enabled"],
|
|
526
|
+
"allow_force_pushes": classic["allow_force_pushes"]["enabled"],
|
|
527
|
+
"allow_deletions": classic["allow_deletions"]["enabled"],
|
|
528
|
+
"required_conversation_resolution": None if classic_conversations is None
|
|
529
|
+
else classic_conversations["enabled"],
|
|
530
|
+
"required_status_checks": None if classic_checks is None else {
|
|
531
|
+
"strict": classic_checks["strict"],
|
|
532
|
+
"contexts": sorted(classic_checks["contexts"]),
|
|
533
|
+
"checks": sorted(({"context": item["context"],
|
|
534
|
+
"app_id": item.get("app_id")}
|
|
535
|
+
for item in classic_checks["checks"]),
|
|
536
|
+
key=lambda item: (item["context"], str(item["app_id"]))),
|
|
537
|
+
},
|
|
538
|
+
"required_pull_request_reviews": None if classic_reviews is None else {
|
|
539
|
+
"required_approving_review_count": classic_reviews["required_approving_review_count"],
|
|
540
|
+
"dismiss_stale_reviews": classic_reviews.get("dismiss_stale_reviews", False),
|
|
541
|
+
"require_code_owner_reviews": classic_reviews.get("require_code_owner_reviews", False),
|
|
542
|
+
"require_last_push_approval": classic_reviews.get("require_last_push_approval", False),
|
|
543
|
+
"required_review_thread_resolution": classic_reviews.get("required_review_thread_resolution", False),
|
|
544
|
+
},
|
|
545
|
+
}
|
|
546
|
+
stable_rules = []
|
|
547
|
+
for item in verified_rules:
|
|
548
|
+
parameters = item.get("parameters")
|
|
549
|
+
if isinstance(parameters, dict):
|
|
550
|
+
parameters = dict(parameters)
|
|
551
|
+
if item["type"] == "required_status_checks":
|
|
552
|
+
parameters["required_status_checks"] = sorted(
|
|
553
|
+
parameters["required_status_checks"],
|
|
554
|
+
key=lambda check: (check["context"], str(check.get("integration_id"))))
|
|
555
|
+
elif item["type"] == "pull_request" and "allowed_merge_methods" in parameters:
|
|
556
|
+
parameters["allowed_merge_methods"] = sorted(parameters["allowed_merge_methods"])
|
|
557
|
+
stable_rules.append({"ruleset_id": item["ruleset_id"],
|
|
558
|
+
"type": item["type"], "parameters": parameters})
|
|
559
|
+
policy_binding = {
|
|
560
|
+
"version": "repository-effective-main/v1",
|
|
561
|
+
"repository": self.repository,
|
|
562
|
+
"classic": classic_binding,
|
|
563
|
+
"rulesets": sorted(stable_rules,
|
|
564
|
+
key=lambda item: (item["ruleset_id"], item["type"],
|
|
565
|
+
json.dumps(item["parameters"], sort_keys=True))),
|
|
566
|
+
"required_checks": policy["required_checks"],
|
|
567
|
+
"required_apps": policy["required_apps"],
|
|
568
|
+
"required_approvals": policy["required_approvals"],
|
|
569
|
+
"required_thread_resolution": required_thread_resolution,
|
|
570
|
+
"strict_checks": True,
|
|
571
|
+
}
|
|
572
|
+
else:
|
|
573
|
+
# Preserve the original synthetic profile's immutable digest semantics.
|
|
574
|
+
policy_binding = {key: value for key, value in policy.items()
|
|
575
|
+
if key != "required_thread_resolution"}
|
|
576
|
+
policy["digest"] = "sha256:" + hashlib.sha256(json.dumps(policy_binding, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
|
|
489
577
|
return policy
|
|
490
578
|
|
|
491
579
|
def delegated_merge_qualification(self, number: int, head_sha: str, *, assurance_profile: str | None = None) -> dict[str, object]:
|
|
492
580
|
"""Fresh protected checks and independent head-bound GitHub reviews."""
|
|
493
|
-
autonomous = assurance_profile
|
|
581
|
+
autonomous = assurance_profile in {"qualification-autonomous-qs@1", "repository-autonomous-qs@1"}
|
|
494
582
|
if assurance_profile is not None and not autonomous:
|
|
495
583
|
raise RunnerError("Unknown delegated merge assurance profile.")
|
|
496
|
-
policy = self._autonomous_effective_policy() if autonomous else None
|
|
584
|
+
policy = self._autonomous_effective_policy(assurance_profile=assurance_profile) if autonomous else None
|
|
497
585
|
if policy is None:
|
|
498
586
|
checks = self.qualification_for_exact_head(number, head_sha)
|
|
499
587
|
else:
|
|
@@ -506,12 +594,24 @@ class GhCliClient:
|
|
|
506
594
|
if not isinstance(raw, dict) or raw.get("headRefOid") != head_sha or raw.get("baseRefName") != "main":
|
|
507
595
|
raise RunnerError("Autonomous PR qualification has a changed head or base.")
|
|
508
596
|
rollup = raw.get("statusCheckRollup")
|
|
509
|
-
if not isinstance(rollup, list) or not rollup
|
|
510
|
-
not isinstance(item, dict) or item.get("status") != "COMPLETED"
|
|
511
|
-
or item.get("conclusion") not in {"SUCCESS", "NEUTRAL", "SKIPPED"} for item in rollup
|
|
512
|
-
):
|
|
597
|
+
if not isinstance(rollup, list) or not rollup:
|
|
513
598
|
raise RunnerError("Autonomous PR required checks are incomplete or failed.")
|
|
514
|
-
observed =
|
|
599
|
+
observed: set[str] = set()
|
|
600
|
+
for item in rollup:
|
|
601
|
+
if not isinstance(item, dict):
|
|
602
|
+
raise RunnerError("Autonomous PR required checks are incomplete or failed.")
|
|
603
|
+
if item.get("__typename") == "StatusContext" or (
|
|
604
|
+
"context" in item and "state" in item and "name" not in item
|
|
605
|
+
):
|
|
606
|
+
name = item.get("context")
|
|
607
|
+
passed = item.get("state") == "SUCCESS"
|
|
608
|
+
else:
|
|
609
|
+
name = item.get("name")
|
|
610
|
+
passed = (item.get("status") == "COMPLETED" and
|
|
611
|
+
item.get("conclusion") in {"SUCCESS", "NEUTRAL", "SKIPPED"})
|
|
612
|
+
if not isinstance(name, str) or not name or not passed:
|
|
613
|
+
raise RunnerError("Autonomous PR required checks are incomplete or failed.")
|
|
614
|
+
observed.add(name)
|
|
515
615
|
if not set(policy["required_checks"]) <= observed:
|
|
516
616
|
raise RunnerError("Autonomous PR is missing effective required checks.")
|
|
517
617
|
if policy["required_apps"]:
|
|
@@ -562,17 +662,37 @@ class GhCliClient:
|
|
|
562
662
|
raise RunnerError("Delegated merge requires protected independent review policy.")
|
|
563
663
|
latest: dict[str, tuple[str, str]] = {}
|
|
564
664
|
for review in reviews:
|
|
565
|
-
if not isinstance(review, dict):
|
|
566
|
-
|
|
567
|
-
reviewer =
|
|
568
|
-
|
|
569
|
-
|
|
665
|
+
if not isinstance(review, dict) or not isinstance(review.get("user"), dict):
|
|
666
|
+
raise RunnerError("Delegated merge review evidence is incomplete.")
|
|
667
|
+
reviewer = review["user"].get("login")
|
|
668
|
+
decision = review.get("state")
|
|
669
|
+
if not isinstance(reviewer, str) or not reviewer or not isinstance(decision, str):
|
|
670
|
+
raise RunnerError("Delegated merge review evidence is incomplete.")
|
|
671
|
+
latest[reviewer] = (decision, str(review.get("commit_id")))
|
|
672
|
+
if any(decision == "CHANGES_REQUESTED" for decision, _commit in latest.values()):
|
|
673
|
+
raise RunnerError("Delegated merge has an unresolved changes-requested review.")
|
|
570
674
|
approvals = sorted(
|
|
571
675
|
reviewer for reviewer, (decision, commit) in latest.items()
|
|
572
676
|
if reviewer != author and decision == "APPROVED" and commit == head_sha
|
|
573
677
|
)
|
|
574
678
|
if len(approvals) < required:
|
|
575
679
|
raise RunnerError("Delegated merge lacks required independent approvals for this exact head.")
|
|
680
|
+
if policy is not None and policy["required_thread_resolution"]:
|
|
681
|
+
owner, repository_name = self.repository.split("/", 1)
|
|
682
|
+
query = ("query { repository(owner:\"" + owner + "\",name:\"" + repository_name
|
|
683
|
+
+ "\") { pullRequest(number:" + str(number)
|
|
684
|
+
+ ") { reviewThreads(first:100) { totalCount nodes { isResolved } } } } }")
|
|
685
|
+
try:
|
|
686
|
+
response = json.loads(self._github("api", "graphql", "-f", "query=" + query))
|
|
687
|
+
threads = response["data"]["repository"]["pullRequest"]["reviewThreads"]
|
|
688
|
+
except (RuntimeError, ValueError, KeyError, TypeError) as error:
|
|
689
|
+
raise RunnerError("Required review-thread resolution could not be read.") from error
|
|
690
|
+
nodes = threads.get("nodes") if isinstance(threads, dict) else None
|
|
691
|
+
if (not isinstance(nodes, list) or not isinstance(threads.get("totalCount"), int)
|
|
692
|
+
or threads["totalCount"] != len(nodes) or len(nodes) >= 100
|
|
693
|
+
or any(not isinstance(item, dict) or item.get("isResolved") is not True
|
|
694
|
+
for item in nodes)):
|
|
695
|
+
raise RunnerError("Delegated merge has unresolved or unreadable review threads.")
|
|
576
696
|
return {**checks, "reviewers": approvals, "required_approvals": required}
|
|
577
697
|
|
|
578
698
|
def merge(self, number: int, *, expected_head_sha: str | None = None) -> None:
|
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
from dataclasses import dataclass
|
|
11
11
|
from datetime import datetime, timedelta, timezone
|
|
12
|
+
import hashlib
|
|
12
13
|
import re
|
|
13
14
|
import sqlite3
|
|
14
15
|
import subprocess
|
|
@@ -22,6 +23,8 @@ _REPOSITORY = re.compile(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+\Z")
|
|
|
22
23
|
_ROLES = frozenset({"IMPLEMENTATION", "FINALIZATION", "RECONCILIATION"})
|
|
23
24
|
AUTONOMOUS_ASSURANCE_PROFILE = "qualification-autonomous-qs@1"
|
|
24
25
|
AUTONOMOUS_ASSURANCE_REPOSITORY = "pcvantol/forge-mission-qualification"
|
|
26
|
+
REPOSITORY_ASSURANCE_PROFILE = "repository-autonomous-qs@1"
|
|
27
|
+
REPOSITORY_ASSURANCE_ID = "repository-autonomous-qs"
|
|
25
28
|
|
|
26
29
|
|
|
27
30
|
@dataclass(frozen=True)
|
|
@@ -41,6 +44,7 @@ class MergeDelegation:
|
|
|
41
44
|
assurance_profile_id: str = ""
|
|
42
45
|
assurance_profile_revision: str = ""
|
|
43
46
|
assurance_policy_digest: str = ""
|
|
47
|
+
target_selection_revision: int = 0
|
|
44
48
|
|
|
45
49
|
def permits(self, *, project_id: str, repository_id: str, mission_id: str,
|
|
46
50
|
mission_revision: str, role: str, base_branch: str,
|
|
@@ -77,16 +81,104 @@ def install_schema(connection: sqlite3.Connection) -> None:
|
|
|
77
81
|
assurance_profile_id TEXT NOT NULL DEFAULT '',
|
|
78
82
|
assurance_profile_revision TEXT NOT NULL DEFAULT '',
|
|
79
83
|
assurance_policy_digest TEXT NOT NULL DEFAULT ''
|
|
84
|
+
,target_selection_revision INTEGER NOT NULL DEFAULT 0
|
|
80
85
|
)""")
|
|
86
|
+
install_selection_schema(connection)
|
|
81
87
|
install_profile_guard(connection)
|
|
82
88
|
|
|
83
89
|
|
|
90
|
+
def install_selection_schema(connection: sqlite3.Connection) -> None:
|
|
91
|
+
"""Append-only owner decisions; revision zero means no decision."""
|
|
92
|
+
connection.execute("""CREATE TABLE IF NOT EXISTS ep_assurance_target_selections (
|
|
93
|
+
project_id TEXT NOT NULL, repository_id TEXT NOT NULL,
|
|
94
|
+
revision INTEGER NOT NULL CHECK(revision > 0),
|
|
95
|
+
github_repository TEXT NOT NULL, binding_digest TEXT NOT NULL,
|
|
96
|
+
profile_id TEXT NOT NULL,
|
|
97
|
+
profile_revision TEXT NOT NULL, actor_reference TEXT NOT NULL,
|
|
98
|
+
policy_digest TEXT NOT NULL, selected_at TEXT NOT NULL,
|
|
99
|
+
PRIMARY KEY(project_id,repository_id,revision)
|
|
100
|
+
)""")
|
|
101
|
+
for operation in ("UPDATE", "DELETE"):
|
|
102
|
+
connection.execute(f"""CREATE TRIGGER IF NOT EXISTS ep_assurance_target_selections_immutable_{operation.lower()}
|
|
103
|
+
BEFORE {operation} ON ep_assurance_target_selections
|
|
104
|
+
BEGIN SELECT RAISE(ABORT, 'assurance target selection is immutable'); END""")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def target_selection(connection: sqlite3.Connection, project_id: str,
|
|
108
|
+
repository_id: str) -> dict[str, object] | None:
|
|
109
|
+
row = connection.execute("""SELECT revision,github_repository,binding_digest,profile_id,
|
|
110
|
+
profile_revision,actor_reference,policy_digest,selected_at
|
|
111
|
+
FROM ep_assurance_target_selections WHERE project_id=? AND repository_id=?
|
|
112
|
+
ORDER BY revision DESC LIMIT 1""", (project_id, repository_id)).fetchone()
|
|
113
|
+
if row is None:
|
|
114
|
+
return None
|
|
115
|
+
return dict(zip(("revision", "github_repository", "binding_digest", "profile_id", "profile_revision",
|
|
116
|
+
"actor_reference", "policy_digest", "selected_at"), row))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _current_binding_digest(connection: sqlite3.Connection, project_id: str,
|
|
120
|
+
repository_id: str) -> str | None:
|
|
121
|
+
row = connection.execute("""SELECT local_root,updated_at FROM ep_local_repository_bindings
|
|
122
|
+
WHERE project_id=? AND repository_id=? AND state='BOUND'""",
|
|
123
|
+
(project_id, repository_id)).fetchone()
|
|
124
|
+
if row is None or not all(isinstance(value, str) and value for value in row):
|
|
125
|
+
return None
|
|
126
|
+
return "sha256:" + hashlib.sha256((row[0] + "\0" + row[1]).encode()).hexdigest()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def select_target_profile(connection: sqlite3.Connection, *, project_id: str,
|
|
130
|
+
repository_id: str, github_repository: str,
|
|
131
|
+
actor_reference: str, policy_digest: str,
|
|
132
|
+
expected_revision: int, revoke: bool = False) -> dict[str, object]:
|
|
133
|
+
"""Record one owner decision after the caller checks actual registration and origin."""
|
|
134
|
+
current = target_selection(connection, project_id, repository_id)
|
|
135
|
+
binding_digest = (str(current["binding_digest"]) if revoke and current else
|
|
136
|
+
_current_binding_digest(connection, project_id, repository_id))
|
|
137
|
+
revision = int(current["revision"]) if current else 0
|
|
138
|
+
if (not isinstance(expected_revision, int) or expected_revision != revision
|
|
139
|
+
or not actor_reference.startswith("local-uid:")
|
|
140
|
+
or _REPOSITORY.fullmatch(github_repository) is None
|
|
141
|
+
or binding_digest is None
|
|
142
|
+
or re.fullmatch(r"sha256:[0-9a-f]{64}", policy_digest) is None
|
|
143
|
+
or current is not None and current["github_repository"] != github_repository
|
|
144
|
+
and (revoke or current["profile_id"] != "")):
|
|
145
|
+
raise ValueError("target profile selection scope or revision changed")
|
|
146
|
+
profile_id, profile_revision = ("", "") if revoke else (REPOSITORY_ASSURANCE_ID, "1")
|
|
147
|
+
connection.execute("""INSERT INTO ep_assurance_target_selections
|
|
148
|
+
(project_id,repository_id,revision,github_repository,binding_digest,profile_id,
|
|
149
|
+
profile_revision,actor_reference,policy_digest,selected_at)
|
|
150
|
+
VALUES(?,?,?,?,?,?,?,?,?,?)""", (project_id, repository_id, revision + 1,
|
|
151
|
+
github_repository, binding_digest, profile_id, profile_revision, actor_reference, policy_digest,
|
|
152
|
+
datetime.now(timezone.utc).isoformat()))
|
|
153
|
+
result = target_selection(connection, project_id, repository_id)
|
|
154
|
+
assert result is not None
|
|
155
|
+
return result
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def target_selection_permits(connection: sqlite3.Connection, grant: MergeDelegation) -> bool:
|
|
159
|
+
if grant.assurance_profile_id != REPOSITORY_ASSURANCE_ID:
|
|
160
|
+
return grant.assurance_profile_id in ("", "qualification-autonomous-qs")
|
|
161
|
+
selected = target_selection(connection, grant.project_id, grant.repository_id)
|
|
162
|
+
return bool(selected and selected["revision"] == grant.target_selection_revision
|
|
163
|
+
and selected["github_repository"] == grant.github_repository
|
|
164
|
+
and selected["binding_digest"] == _current_binding_digest(
|
|
165
|
+
connection, grant.project_id, grant.repository_id)
|
|
166
|
+
and selected["profile_id"] == grant.assurance_profile_id
|
|
167
|
+
and selected["profile_revision"] == grant.assurance_profile_revision
|
|
168
|
+
and selected["policy_digest"] == grant.assurance_policy_digest)
|
|
169
|
+
|
|
170
|
+
|
|
84
171
|
def install_profile_guard(connection: sqlite3.Connection) -> None:
|
|
172
|
+
columns = {str(row[1]) for row in connection.execute("PRAGMA table_info(ep_merge_delegations)")}
|
|
173
|
+
if "target_selection_revision" not in columns:
|
|
174
|
+
connection.execute("ALTER TABLE ep_merge_delegations ADD COLUMN target_selection_revision INTEGER NOT NULL DEFAULT 0")
|
|
175
|
+
connection.execute("DROP TRIGGER IF EXISTS ep_merge_delegations_profile_immutable")
|
|
85
176
|
connection.execute("""CREATE TRIGGER IF NOT EXISTS ep_merge_delegations_profile_immutable
|
|
86
|
-
BEFORE UPDATE OF assurance_profile_id, assurance_profile_revision, assurance_policy_digest ON ep_merge_delegations
|
|
177
|
+
BEFORE UPDATE OF assurance_profile_id, assurance_profile_revision, assurance_policy_digest, target_selection_revision ON ep_merge_delegations
|
|
87
178
|
WHEN NEW.assurance_profile_id != OLD.assurance_profile_id
|
|
88
179
|
OR NEW.assurance_profile_revision != OLD.assurance_profile_revision
|
|
89
180
|
OR NEW.assurance_policy_digest != OLD.assurance_policy_digest
|
|
181
|
+
OR NEW.target_selection_revision != OLD.target_selection_revision
|
|
90
182
|
BEGIN SELECT RAISE(ABORT, 'merge delegation assurance profile is immutable'); END""")
|
|
91
183
|
|
|
92
184
|
|
|
@@ -128,12 +220,25 @@ def reserve(connection: sqlite3.Connection, *, delegation_id: str, actor_referen
|
|
|
128
220
|
now = datetime.now(timezone.utc)
|
|
129
221
|
if expiry.tzinfo is None or not now < expiry <= now + timedelta(days=7):
|
|
130
222
|
raise ValueError("merge delegation expiry must be within seven days")
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
223
|
+
selection_revision = 0
|
|
224
|
+
if assurance_profile == AUTONOMOUS_ASSURANCE_PROFILE:
|
|
225
|
+
if github_repository != AUTONOMOUS_ASSURANCE_REPOSITORY:
|
|
226
|
+
raise ValueError("autonomous assurance profile is unavailable for this repository")
|
|
227
|
+
profile_id, profile_revision = "qualification-autonomous-qs", "1"
|
|
228
|
+
elif assurance_profile == REPOSITORY_ASSURANCE_PROFILE:
|
|
229
|
+
selection = target_selection(connection, project_id, repository_id)
|
|
230
|
+
if (selection is None or selection["github_repository"] != github_repository
|
|
231
|
+
or selection["binding_digest"] != _current_binding_digest(connection, project_id, repository_id)
|
|
232
|
+
or selection["profile_id"] != REPOSITORY_ASSURANCE_ID
|
|
233
|
+
or selection["profile_revision"] != "1"
|
|
234
|
+
or selection["policy_digest"] != assurance_policy_digest):
|
|
235
|
+
raise ValueError("repository assurance profile lacks a current owner selection")
|
|
236
|
+
selection_revision = int(selection["revision"])
|
|
237
|
+
profile_id, profile_revision = REPOSITORY_ASSURANCE_ID, "1"
|
|
238
|
+
elif assurance_profile is None:
|
|
239
|
+
profile_id, profile_revision = "", ""
|
|
240
|
+
else:
|
|
135
241
|
raise ValueError("autonomous assurance profile is unavailable for this repository")
|
|
136
|
-
profile_id, profile_revision = ("qualification-autonomous-qs", "1") if assurance_profile else ("", "")
|
|
137
242
|
if (assurance_profile and re.fullmatch(r"sha256:[0-9a-f]{64}", assurance_policy_digest) is None
|
|
138
243
|
or not assurance_profile and assurance_policy_digest):
|
|
139
244
|
raise ValueError("merge delegation assurance policy digest is invalid")
|
|
@@ -141,15 +246,17 @@ def reserve(connection: sqlite3.Connection, *, delegation_id: str, actor_referen
|
|
|
141
246
|
connection.execute("""INSERT INTO ep_merge_delegations
|
|
142
247
|
(delegation_id,actor_reference,project_id,repository_id,github_repository,mission_id,
|
|
143
248
|
mission_revision,base_branch,roles,expires_at,created_at,activated_at,revoked_at,
|
|
144
|
-
assurance_profile_id,assurance_profile_revision,assurance_policy_digest)
|
|
145
|
-
VALUES(?,?,?,?,?,?,?,?,?,?,?,NULL,NULL
|
|
249
|
+
assurance_profile_id,assurance_profile_revision,assurance_policy_digest,target_selection_revision)
|
|
250
|
+
VALUES(?,?,?,?,?,?,?,?,?,?,?,NULL,NULL,?,?,?,?)""", (
|
|
146
251
|
delegation_id, actor_reference.strip(), project_id, repository_id, github_repository, '',
|
|
147
252
|
'', base_branch, json.dumps(selected), expires_at,
|
|
148
253
|
datetime.now(timezone.utc).isoformat(), profile_id, profile_revision, assurance_policy_digest,
|
|
254
|
+
selection_revision,
|
|
149
255
|
))
|
|
150
256
|
return MergeDelegation(delegation_id, actor_reference.strip(), project_id,
|
|
151
257
|
repository_id, github_repository, '', '', base_branch,
|
|
152
|
-
selected, expires_at, None, None, profile_id, profile_revision,
|
|
258
|
+
selected, expires_at, None, None, profile_id, profile_revision,
|
|
259
|
+
assurance_policy_digest, selection_revision)
|
|
153
260
|
|
|
154
261
|
|
|
155
262
|
def activate(connection: sqlite3.Connection, *, delegation_id: str,
|
|
@@ -165,6 +272,8 @@ def activate(connection: sqlite3.Connection, *, delegation_id: str,
|
|
|
165
272
|
expiry = datetime.fromisoformat(grant.expires_at)
|
|
166
273
|
if expiry <= datetime.now(timezone.utc):
|
|
167
274
|
raise ValueError("Merge delegation has expired")
|
|
275
|
+
if not target_selection_permits(connection, grant):
|
|
276
|
+
raise ValueError("Merge delegation target selection is no longer current")
|
|
168
277
|
changed = connection.execute("""UPDATE ep_merge_delegations SET mission_id=?,mission_revision=?,activated_at=?
|
|
169
278
|
WHERE delegation_id=? AND activated_at IS NULL AND revoked_at IS NULL""",
|
|
170
279
|
(mission_id, mission_revision, datetime.now(timezone.utc).isoformat(), delegation_id)).rowcount
|
|
@@ -180,7 +289,8 @@ def load(connection: sqlite3.Connection, delegation_id: str) -> MergeDelegation
|
|
|
180
289
|
return None
|
|
181
290
|
row = connection.execute("""SELECT delegation_id,actor_reference,project_id,
|
|
182
291
|
repository_id,github_repository,mission_id,mission_revision,base_branch,roles,expires_at,
|
|
183
|
-
activated_at,revoked_at,assurance_profile_id,assurance_profile_revision,assurance_policy_digest
|
|
292
|
+
activated_at,revoked_at,assurance_profile_id,assurance_profile_revision,assurance_policy_digest,
|
|
293
|
+
target_selection_revision
|
|
184
294
|
FROM ep_merge_delegations WHERE delegation_id=?""",
|
|
185
295
|
(delegation_id,)).fetchone()
|
|
186
296
|
if row is None:
|
|
@@ -192,14 +302,18 @@ def load(connection: sqlite3.Connection, delegation_id: str) -> MergeDelegation
|
|
|
192
302
|
return None
|
|
193
303
|
if not isinstance(roles, list) or not roles or any(role not in _ROLES for role in roles):
|
|
194
304
|
return None
|
|
195
|
-
if (row[12], row[13]) not in {("", ""), ("qualification-autonomous-qs", "1")
|
|
305
|
+
if (row[12], row[13]) not in {("", ""), ("qualification-autonomous-qs", "1"),
|
|
306
|
+
(REPOSITORY_ASSURANCE_ID, "1")}:
|
|
196
307
|
return None
|
|
197
|
-
if row[12] and row[4] != AUTONOMOUS_ASSURANCE_REPOSITORY:
|
|
308
|
+
if row[12] == "qualification-autonomous-qs" and row[4] != AUTONOMOUS_ASSURANCE_REPOSITORY:
|
|
198
309
|
return None
|
|
199
310
|
if (row[12] and re.fullmatch(r"sha256:[0-9a-f]{64}", row[14]) is None
|
|
200
311
|
or not row[12] and row[14] != ""):
|
|
201
312
|
return None
|
|
202
|
-
|
|
313
|
+
if (not isinstance(row[15], int) or row[15] < 0
|
|
314
|
+
or (row[12] == REPOSITORY_ASSURANCE_ID) != (row[15] > 0)):
|
|
315
|
+
return None
|
|
316
|
+
return MergeDelegation(*row[:8], tuple(roles), row[9], row[10], row[11], row[12], row[13], row[14], row[15])
|
|
203
317
|
|
|
204
318
|
|
|
205
319
|
def revoke(connection: sqlite3.Connection, delegation_id: str, *, actor_reference: str) -> bool:
|
|
@@ -12,7 +12,7 @@ from .storage import ENGINEERING_STORAGE_SCHEMA_VERSION
|
|
|
12
12
|
|
|
13
13
|
SEMVER = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$")
|
|
14
14
|
CONTRACT = re.compile(r"^(\d{4})\.(0[1-9]|1[0-2])$")
|
|
15
|
-
CURRENT_PLATFORM_VERSION = "2.3.
|
|
15
|
+
CURRENT_PLATFORM_VERSION = "2.3.89"
|
|
16
16
|
MANIFEST_FIELDS = frozenset(
|
|
17
17
|
{
|
|
18
18
|
"platform_version",
|