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