From 63daf78c3c6feb2e3c4951b87a5fb11b1712c45c Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Tue, 13 Oct 2026 02:48:03 +0000 Subject: [PATCH 1/3] Verify installed SDK identity in replay conformance --- README.md | 8 ++++ src/durable_workflow/replay_conformance.py | 51 +++++++++++++++------- tests/test_replay_conformance.py | 47 ++++++++++++++++++-- 3 files changed, 87 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 1d6baeb..9187b3d 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,14 @@ for immutable context, operation policies, shielded cleanup and recovery. ## Development +The installed `durable-workflow-replay-conformance` command runs an offline +Python replay shard. Its `sdk-python` artifact pin must match the installed +distribution version reported by `importlib.metadata.version("durable-workflow")`. +The report records that installed identity separately from the declared platform +tuple and fails if metadata is missing or the SDK pin differs. Replay success +does not prove installation or execution of the other declared artifacts; the +aggregate runner supplies its own verified installation receipts. + ```bash pip install -e '.[dev]' ruff check src/ tests/ diff --git a/src/durable_workflow/replay_conformance.py b/src/durable_workflow/replay_conformance.py index 9ed5f6d..721d9d9 100644 --- a/src/durable_workflow/replay_conformance.py +++ b/src/durable_workflow/replay_conformance.py @@ -15,6 +15,7 @@ import sys from collections.abc import Mapping, Sequence from datetime import datetime, timezone +from importlib import metadata from pathlib import Path from typing import Any @@ -204,6 +205,17 @@ def compose_report( started_at = _timestamp() versions = _canonical_artifact_metadata(dict(artifact_versions or {})) sources = _canonical_artifact_metadata(dict(artifact_sources or {})) + try: + installed_version = metadata.version("durable-workflow") + except metadata.PackageNotFoundError: + installed_version = None + identity = { + "runtime": "sdk-python", + "package": "durable-workflow", + "version": installed_version, + "version_source": "installed_distribution_metadata" if installed_version else "unavailable", + "declared_version": _artifact_metadata(versions, "sdk-python"), + } scenario_results: dict[str, dict[str, Any]] = {} findings: list[dict[str, Any]] = [] finding_links: dict[str, list[dict[str, Any]]] = {} @@ -212,7 +224,7 @@ def compose_report( scenario_results, findings, finding_links, - _published_artifact_scenario(versions, sources), + _published_artifact_scenario(versions, sources, installed_version), ) for family, scenario_id in COMPLETED_SCENARIOS.items(): @@ -242,6 +254,9 @@ def compose_report( has_failures = any( scenario.get("status") != "pass" for scenario in scenario_results.values() ) + for scenario in scenario_results.values(): + scenario["implementation_identity"] = dict(identity) + scenario["artifact_tuple_evidence"] = "declared" return { "schema": RESULT_SCHEMA, @@ -252,6 +267,8 @@ def compose_report( "finished_at": _timestamp(), "artifact_versions": versions, "artifact_sources": sources, + "artifact_tuple_evidence": "declared", + "implementation_identity": identity, "runtime_matrix": { "runtimes": ["sdk-python"], }, @@ -307,6 +324,7 @@ def _append_scenario( def _published_artifact_scenario( artifact_versions: Mapping[str, str], artifact_sources: Mapping[str, str], + installed_version: str | None, ) -> dict[str, Any]: missing_versions: list[str] = [] missing_sources: list[str] = [] @@ -338,14 +356,14 @@ def _published_artifact_scenario( untrusted_sources[artifact] = source missing = sorted(set(missing_versions + missing_sources)) - status = ( - "pass" - if not missing - and not rejected_versions - and not forbidden_sources - and not untrusted_sources - else "fail" - ) + declarations_valid = not missing and not rejected_versions and not forbidden_sources and not untrusted_sources + declared_version = _artifact_metadata(artifact_versions, "sdk-python") + identity_error = None + if not installed_version: + identity_error = "installed_sdk_metadata_missing" + elif declared_version != installed_version: + identity_error = "installed_sdk_version_mismatch" + status = "pass" if declarations_valid and identity_error is None else "fail" return _scenario( "published_artifact_install_only", @@ -361,8 +379,14 @@ def _published_artifact_scenario( "rejected_versions": rejected_versions, "forbidden_sources": forbidden_sources, "untrusted_sources": untrusted_sources, - "published_artifacts_only": status == "pass", - "published_install_tuple_proven": status == "pass", + "declared_artifacts_published_only": bool(declarations_valid), + "declared_sdk_version": declared_version, + "installed_sdk_version": installed_version, + "sdk_identity_error": identity_error, + "sdk_version_matches": bool(installed_version and declared_version == installed_version), + "artifact_tuple_evidence": "declared", + "published_artifacts_only": False, + "published_install_tuple_proven": False, }, ) @@ -589,11 +613,6 @@ def _scenario( "scenario_id": scenario_id, "status": status, "published_artifact_versions": dict(artifact_versions), - "implementation_identity": { - "runtime": "sdk-python", - "package": "durable-workflow", - "version": _artifact_metadata(artifact_versions, "sdk-python"), - }, "runtime_matrix": { "runtimes": ["sdk-python"], }, diff --git a/tests/test_replay_conformance.py b/tests/test_replay_conformance.py index 86d16b2..693dfb0 100644 --- a/tests/test_replay_conformance.py +++ b/tests/test_replay_conformance.py @@ -1,9 +1,12 @@ from __future__ import annotations import json +from importlib import metadata from pathlib import Path from typing import Any +import pytest + from durable_workflow.replay_conformance import ( COVERAGE_SCOPE, RESULT_SCHEMA, @@ -25,6 +28,11 @@ def artifact_versions() -> dict[str, str]: } +@pytest.fixture(autouse=True) +def installed_sdk_metadata(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(metadata, "version", lambda package: "0.4.73") + + def artifact_sources() -> dict[str, str]: return { "server": "docker_image", @@ -81,7 +89,7 @@ def test_compose_report_emits_python_replay_conformance_shard() -> None: assert indexed[scenario_id]["status"] == "pass" -def test_report_proves_published_artifact_install_policy() -> None: +def test_report_validates_declared_tuple_without_claiming_installation_proof() -> None: report = compose_report( artifact_versions=artifact_versions(), artifact_sources=artifact_sources(), @@ -90,13 +98,46 @@ def test_report_proves_published_artifact_install_policy() -> None: observed = published["observed_outputs"] assert published["status"] == "pass" - assert observed["published_artifacts_only"] is True - assert observed["published_install_tuple_proven"] is True + assert observed["published_artifacts_only"] is False + assert observed["published_install_tuple_proven"] is False + assert observed["declared_artifacts_published_only"] is True + assert observed["artifact_tuple_evidence"] == "declared" + assert observed["installed_sdk_version"] == "0.4.73" + assert observed["sdk_version_matches"] is True + assert observed["sdk_identity_error"] is None + assert report["implementation_identity"]["version"] == "0.4.73" assert observed["missing_artifacts"] == [] assert observed["artifact_sources"]["server"] == "docker_image" assert observed["artifact_sources"]["sdk-python"] == "pypi_package" +def test_report_rejects_declared_sdk_version_mismatch(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(metadata, "version", lambda package: "2.5.10") + report = compose_report(artifact_versions=artifact_versions(), artifact_sources=artifact_sources()) + install = scenarios(report)["published_artifact_install_only"] + assert report["outcome"] == "fail" + assert install["status"] == "fail" + assert install["observed_outputs"]["installed_sdk_version"] == "2.5.10" + assert install["observed_outputs"]["declared_sdk_version"] == "0.4.73" + assert install["observed_outputs"]["sdk_identity_error"] == "installed_sdk_version_mismatch" + assert install["observed_outputs"]["published_install_tuple_proven"] is False + assert all(scenario["implementation_identity"]["version"] == "2.5.10" for scenario in report["scenario_results"]) + assert report["finding_links"]["published_artifact_install_only"] + + +def test_report_rejects_missing_installed_sdk_metadata(monkeypatch: pytest.MonkeyPatch) -> None: + def missing_metadata(package: str) -> str: + raise metadata.PackageNotFoundError(package) + + monkeypatch.setattr(metadata, "version", missing_metadata) + report = compose_report(artifact_versions=artifact_versions(), artifact_sources=artifact_sources()) + install = scenarios(report)["published_artifact_install_only"] + assert report["outcome"] == "fail" + assert install["observed_outputs"]["sdk_identity_error"] == "installed_sdk_metadata_missing" + assert install["implementation_identity"]["version"] is None + assert install["observed_outputs"]["published_install_tuple_proven"] is False + + def test_report_rejects_missing_versions_and_sources() -> None: report = compose_report( artifact_versions={"sdk-python": "0.4.73"}, From 6983d3f5810ae9ce21bfe1e623d9fc178626ce32 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Tue, 13 Oct 2026 02:49:15 +0000 Subject: [PATCH 2/3] Prepare stable 2.5.11 replay identity correction --- CHANGELOG.md | 7 +++++++ pyproject.toml | 6 +++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82da13f..a4b55bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.5.11] - 2026-10-13 + +### Fixed +- Verify the installed Python distribution identity in offline replay conformance + reports. Mismatched SDK pins or missing metadata fail explicitly; a declared + platform tuple no longer claims verified artifact installation. + ## [2.5.10] - 2026-10-13 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index 5138760..a4568b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "durable-workflow" -version = "2.5.10" +version = "2.5.11" description = "Python client and worker SDK for Durable Workflow Cloud and self-hosted Server" readme = "README.md" requires-python = ">=3.10" @@ -71,8 +71,8 @@ durable-workflow-replay-conformance = "durable_workflow.replay_conformance:main" durable-workflow-workflow-updates-conformance = "durable_workflow.workflow_updates_conformance:main" [tool.durable-workflow] -product-train = "2.5.10" -registry-version = "2.5.10" +product-train = "2.5.11" +registry-version = "2.5.11" supported-server-versions = "2.5.16" worker-protocol-version = "1.20" control-plane-version = "2" From 8cf7c82ef81ef9cb7faef39b817e77d710850ac1 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Tue, 13 Oct 2026 03:11:26 +0000 Subject: [PATCH 3/3] Retain installed replay identity command regression --- ...nstalled-replay-sdk-identity-mismatch.json | 48 +++++++++++++++++++ tests/test_replay_regression_corpus.py | 26 ++++++++++ 2 files changed, 74 insertions(+) create mode 100644 tests/fixtures/replay_regressions/installed-replay-sdk-identity-mismatch.json diff --git a/tests/fixtures/replay_regressions/installed-replay-sdk-identity-mismatch.json b/tests/fixtures/replay_regressions/installed-replay-sdk-identity-mismatch.json new file mode 100644 index 0000000..92747f7 --- /dev/null +++ b/tests/fixtures/replay_regressions/installed-replay-sdk-identity-mismatch.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://durable-workflow.github.io/schemas/replay-regression-v1.json", + "fixture_schema": "durable-workflow.replay-regression/v1", + "id": "installed-replay-sdk-identity-mismatch", + "protocol_version": "1.20", + "bindings": ["python"], + "workflow": { + "type": "sdk.replay-conformance-report", + "input": [{ + "artifact_versions": { + "server": "2.6.1", + "cli": "2.2.3", + "workflow-php": "2.5.12", + "sdk-python": "2.5.9", + "waterline": "2.10.0" + }, + "artifact_sources": { + "server": "published_docker_image", + "cli": "github_release_asset", + "workflow-php": "packagist_package", + "sdk-python": "pypi_package", + "waterline": "packagist_package" + } + }] + }, + "command_sequence": [{ + "type": "replay_conformance_report", + "outcome": "fail", + "sdk_identity_error": "installed_sdk_version_mismatch", + "declared_sdk_version": "2.5.9", + "published_install_tuple_proven": false + }], + "expected": { + "command_sequence": [{ + "type": "replay_conformance_report", + "outcome": "fail", + "sdk_identity_error": "installed_sdk_version_mismatch", + "declared_sdk_version": "2.5.9", + "published_install_tuple_proven": false + }] + }, + "provenance": { + "issue": "https://github.com/durable-workflow/sdk-python/issues/139", + "observed_installed_sdk": "2.5.10", + "observed_old_outcome": "pass", + "scope": "offline replay conformance report; other artifacts are declarations" + } +} diff --git a/tests/test_replay_regression_corpus.py b/tests/test_replay_regression_corpus.py index 1fab6e0..4a75623 100644 --- a/tests/test_replay_regression_corpus.py +++ b/tests/test_replay_regression_corpus.py @@ -2,6 +2,7 @@ import json from collections.abc import Mapping, Sequence +from importlib import metadata from pathlib import Path from typing import Any from unittest.mock import AsyncMock @@ -11,6 +12,7 @@ from durable_workflow import Replayer, Worker, serializer, workflow from durable_workflow.client import PROTOCOL_VERSION, Client, WorkflowExecution, WorkflowStreamAppendItem from durable_workflow.errors import NonDeterministicReplayError, WorkflowCancelled, WorkflowPayloadDecodeError +from durable_workflow.replay_conformance import compose_report from durable_workflow.workflow import ( LocalActivityExecutionAborted, WorkflowContext, @@ -371,6 +373,27 @@ def _execute_fixture(fixture: dict[str, Any]) -> list[dict[str, Any]]: assert isinstance(workflow, dict) workflow_type = workflow.get("type") assert isinstance(workflow_type, str) and workflow_type + if workflow_type == "sdk.replay-conformance-report": + # This public command executes the SDK's offline replay scenarios. Its + # regression input is artifact declarations, not an invented history. + assert "history" not in fixture + inputs = workflow.get("input") + assert isinstance(inputs, list) and len(inputs) == 1 and isinstance(inputs[0], dict) + report = compose_report(**inputs[0]) + install = next(scenario for scenario in report["scenario_results"] + if scenario["scenario_id"] == "published_artifact_install_only") + observed = install["observed_outputs"] + commands = [{ + "type": "replay_conformance_report", + "outcome": report["outcome"], + "sdk_identity_error": observed.get("sdk_identity_error"), + "declared_sdk_version": observed.get("declared_sdk_version"), + "published_install_tuple_proven": observed["published_install_tuple_proven"], + }] + _assert_matches(fixture["command_sequence"], commands, f"{fixture['id']}.command_sequence") + _assert_matches(fixture["expected"], {"command_sequence": commands}, f"{fixture['id']}.expected") + assert report["implementation_identity"]["version"] == metadata.version("durable-workflow") + return commands assert workflow_type in WORKFLOW_TYPES, ( f"replay fixture workflow {workflow_type!r} has no Python implementation; " "register its reproducer workflow in WORKFLOWS" @@ -462,6 +485,9 @@ def test_checked_in_replay_regression_corpus_uses_official_replayer( assert isinstance(workflow, dict) expected = fixture.get("expected") assert isinstance(expected, dict) and expected + if workflow.get("type") == "sdk.replay-conformance-report": + _execute_fixture(fixture) + return expected_error = expected.get("error") expected_replay_error = fixture.get("expected_replay_error") if isinstance(expected_replay_error, dict):