Skip to content

history_bundle_verify accepts retention-pruned history bundles as ok #228

Closed

Description

@rmcdaniel

Problem

durable_workflow.history_bundle_verify (published 2.12.3, and source at main 9094996) reports a retention-pruned history-export bundle as status: ok with zero findings. PHP Core's BundleIntegrityVerifier fails the same bundle with the error history.details_pruned. Python replay verification would therefore accept a bundle that has no history to replay or import.

Parity defect filed at General's request from durable-workflow/workflow#856 (PHP fix in draft PR durable-workflow/workflow#980). The PHP fix is not yet released; the bundle below was produced by the #980 head (2e562997).

Bundle contract (additive, from workflow#980)

After WorkflowRunRetentionCleanup::pruneRun(), HistoryExport::forRun() exports a pruned terminal run with:

  • top-level history_complete: false, even though workflow.status stays terminal
  • empty history_events
  • workflow.details_state: "pruned" ("retained" on every other bundle) and workflow.details_pruned_at (a UTC timestamp, or null)
  • dedupe_key = the pre-prune key + :details-pruned:{details_pruned_at}

Exact reproduction

  1. Produce the bundle with the PHP Core #980 head, in a PHP 8.4 / PostgreSQL 17 / Redis 7 container. A real TestGreetingWorkflow run completed (workflow → activity → workflow), was pruned, then exported. The full bundle is below.
  2. Verify it with the normally installed SDK:
docker run --rm -v "$PWD/pruned-bundle.json:/b.json:ro" python:3.12-slim sh -c '
  pip install -q durable-workflow==2.12.3
  cd /tmp && python -I -c "
import json, durable_workflow.history_bundle_verify as m
r = m.verify_bundle_json(open(\"/b.json\").read())
print(r[\"status\"], json.dumps(r[\"summary\"]), json.dumps(r[\"findings\"]))"'

Actual (Python 2.12.3):

ok {"findings": 0, "errors": 0, "warnings": 0, "info": 0, "findings_by_rule": {}} []

The checksum matches: 8fa57d07aa2835e1210f3d5a840a2d46e63b4d918e15689dd8818f9c7b1101ac, recomputed identically, so canonicalization of the new fields already agrees with PHP.

Expected (PHP BundleIntegrityVerifier, same bundle): status: failed with one error:

{
  "rule": "history.details_pruned",
  "severity": "error",
  "message": "Retention cleanup pruned this run's details at 2026-11-02T21:48:14.952676Z; the bundle has no history to replay or import.",
  "path": "workflow.details_state",
  "context": {
    "details_pruned_at": "2026-11-02T21:48:14.952676Z",
    "history_complete": false
  }
}

Acceptance

  • verify_bundle reports history.details_pruned as an error when workflow.details_state == "pruned", with the same rule id, path workflow.details_state, and details_pruned_at / history_complete in the context. The verdict becomes failed, matching PHP.
  • Bundles with details_state: "retained", or without the key (older exports), are unchanged.
  • A regression test uses the bundle below, or an equivalent fixture.
  • Python replay-verify / batch simulation surfaces the failure, and does not report the bundle as replay-ready.

Older pruned exports, produced before workflow#980, carry no marker and can't be detected from the bundle alone. That is out of scope.

pruned-bundle.json (PHP Core #980 head 2e562997, test data only)
{
    "schema": "durable-workflow.v2.history-export",
    "schema_version": 2,
    "exported_at": "2026-11-02T21:48:14.974261Z",
    "dedupe_key": "01m6f3qvzpvz4jth4ag0aczfv8:6:2026-11-02T21:48:14.702328Z:details-pruned:2026-11-02T21:48:14.952676Z",
    "history_complete": false,
    "workflow": {
        "instance_id": "pruned-parity-repro",
        "run_id": "01m6f3qvzpvz4jth4ag0aczfv8",
        "run_number": 1,
        "is_current_run": true,
        "current_run_id": "01m6f3qvzpvz4jth4ag0aczfv8",
        "current_run_source": "run_order_fallback",
        "workflow_type": "test-greeting-workflow",
        "workflow_class": "Tests\\Fixtures\\V2\\TestGreetingWorkflow",
        "source_runtime": "embedded",
        "source_process_class": "application_process",
        "business_key": null,
        "visibility_labels": [],
        "memo": [],
        "memo_payload": {
            "codec": "avro",
            "blob": "wwHioz3/VYAiNw4A"
        },
        "search_attributes": [],
        "status": "completed",
        "status_bucket": "completed",
        "closed_reason": "completed",
        "archived_at": null,
        "archive_command_id": null,
        "archive_reason": null,
        "compatibility": null,
        "connection": null,
        "queue": "default",
        "last_history_sequence": 6,
        "started_at": "2026-11-02T21:48:10.415185Z",
        "closed_at": "2026-11-02T21:48:14.696276Z",
        "last_progress_at": "2026-11-02T21:48:14.702328Z",
        "details_state": "pruned",
        "details_pruned_at": "2026-11-02T21:48:14.952676Z"
    },
    "payloads": {
        "codec": "avro",
        "arguments": {
            "available": true,
            "data": "wwHioz3/VYAiNwwCCgZBZGEA"
        },
        "output": {
            "available": true,
            "codec": "avro",
            "data": "wwHioz3/VYAiNw4GEGdyZWV0aW5nChZIZWxsbywgQWRhIRZ3b3JrZmxvd19pZAomcHJ1bmVkLXBhcml0eS1yZXBybwxydW5faWQKNDAxbTZmM3F2enB2ejRqdGg0YWcwYWN6ZnY4AA=="
        }
    },
    "summary": null,
    "selected_run": {
        "waits_projection_source": "workflow_run_waits",
        "timeline_projection_source": "workflow_run_timeline_entries",
        "timers_projection_source": "workflow_run_timer_entries",
        "timers_projection_rebuild_reasons": [],
        "lineage_projection_source": "workflow_run_lineage_entries"
    },
    "history_events": [],
    "waits": [],
    "timeline": [],
    "linked_intakes_scope": "selected_run",
    "linked_intakes": [],
    "commands": [],
    "signals": [],
    "updates": [],
    "tasks": [],
    "activities": [],
    "timers": [],
    "failures": [],
    "links": {
        "projection_source": "workflow_run_lineage_entries",
        "parents": [],
        "children": []
    },
    "redaction": {
        "applied": false,
        "policy": null,
        "paths": []
    },
    "codec_schemas": {
        "avro": {
            "encoding": "base64-avro-single-object",
            "framing": "single_object",
            "magic_hex": "c301",
            "current_fingerprint": "e2a33dff55802237",
            "writer_schemas": {
                "e2a33dff55802237": {
                    "schema": "{\"type\":\"record\",\"name\":\"Value\",\"namespace\":\"durable_workflow.protocol\",\"fields\":[{\"name\":\"value\",\"type\":[\"null\",{\"type\":\"record\",\"name\":\"BooleanValue\",\"fields\":[{\"name\":\"boolean\",\"type\":\"boolean\"}]},{\"type\":\"record\",\"name\":\"LongValue\",\"fields\":[{\"name\":\"long\",\"type\":\"long\"}]},{\"type\":\"record\",\"name\":\"DoubleValue\",\"fields\":[{\"name\":\"double\",\"type\":\"double\"}]},{\"type\":\"record\",\"name\":\"BytesValue\",\"fields\":[{\"name\":\"bytes\",\"type\":\"bytes\"}]},{\"type\":\"record\",\"name\":\"StringValue\",\"fields\":[{\"name\":\"string\",\"type\":\"string\"}]},{\"type\":\"record\",\"name\":\"ArrayValue\",\"fields\":[{\"name\":\"items\",\"type\":{\"type\":\"array\",\"items\":\"Value\"}}]},{\"type\":\"record\",\"name\":\"MapValue\",\"fields\":[{\"name\":\"entries\",\"type\":{\"type\":\"map\",\"values\":\"Value\"}}]}]}]}",
                    "fingerprint": "crc64-avro:e2a33dff55802237"
                }
            }
        }
    },
    "payload_manifest": {
        "version": 1,
        "entries": [
            {
                "path": "workflow.memo_payload",
                "codec": "avro",
                "available": true,
                "redacted": false,
                "encoding": "base64-avro-single-object",
                "avro_framing": "single_object",
                "avro_prefix_hex": "c301",
                "writer_schema": "{\"type\":\"record\",\"name\":\"Value\",\"namespace\":\"durable_workflow.protocol\",\"fields\":[{\"name\":\"value\",\"type\":[\"null\",{\"type\":\"record\",\"name\":\"BooleanValue\",\"fields\":[{\"name\":\"boolean\",\"type\":\"boolean\"}]},{\"type\":\"record\",\"name\":\"LongValue\",\"fields\":[{\"name\":\"long\",\"type\":\"long\"}]},{\"type\":\"record\",\"name\":\"DoubleValue\",\"fields\":[{\"name\":\"double\",\"type\":\"double\"}]},{\"type\":\"record\",\"name\":\"BytesValue\",\"fields\":[{\"name\":\"bytes\",\"type\":\"bytes\"}]},{\"type\":\"record\",\"name\":\"StringValue\",\"fields\":[{\"name\":\"string\",\"type\":\"string\"}]},{\"type\":\"record\",\"name\":\"ArrayValue\",\"fields\":[{\"name\":\"items\",\"type\":{\"type\":\"array\",\"items\":\"Value\"}}]},{\"type\":\"record\",\"name\":\"MapValue\",\"fields\":[{\"name\":\"entries\",\"type\":{\"type\":\"map\",\"values\":\"Value\"}}]}]}]}",
                "writer_schema_fingerprint": "crc64-avro:e2a33dff55802237",
                "diagnostic": null
            },
            {
                "path": "payloads.arguments.data",
                "codec": "avro",
                "available": true,
                "redacted": false,
                "encoding": "base64-avro-single-object",
                "avro_framing": "single_object",
                "avro_prefix_hex": "c301",
                "writer_schema": "{\"type\":\"record\",\"name\":\"Value\",\"namespace\":\"durable_workflow.protocol\",\"fields\":[{\"name\":\"value\",\"type\":[\"null\",{\"type\":\"record\",\"name\":\"BooleanValue\",\"fields\":[{\"name\":\"boolean\",\"type\":\"boolean\"}]},{\"type\":\"record\",\"name\":\"LongValue\",\"fields\":[{\"name\":\"long\",\"type\":\"long\"}]},{\"type\":\"record\",\"name\":\"DoubleValue\",\"fields\":[{\"name\":\"double\",\"type\":\"double\"}]},{\"type\":\"record\",\"name\":\"BytesValue\",\"fields\":[{\"name\":\"bytes\",\"type\":\"bytes\"}]},{\"type\":\"record\",\"name\":\"StringValue\",\"fields\":[{\"name\":\"string\",\"type\":\"string\"}]},{\"type\":\"record\",\"name\":\"ArrayValue\",\"fields\":[{\"name\":\"items\",\"type\":{\"type\":\"array\",\"items\":\"Value\"}}]},{\"type\":\"record\",\"name\":\"MapValue\",\"fields\":[{\"name\":\"entries\",\"type\":{\"type\":\"map\",\"values\":\"Value\"}}]}]}]}",
                "writer_schema_fingerprint": "crc64-avro:e2a33dff55802237",
                "diagnostic": null
            },
            {
                "path": "payloads.output.data",
                "codec": "avro",
                "available": true,
                "redacted": false,
                "encoding": "base64-avro-single-object",
                "avro_framing": "single_object",
                "avro_prefix_hex": "c301",
                "writer_schema": "{\"type\":\"record\",\"name\":\"Value\",\"namespace\":\"durable_workflow.protocol\",\"fields\":[{\"name\":\"value\",\"type\":[\"null\",{\"type\":\"record\",\"name\":\"BooleanValue\",\"fields\":[{\"name\":\"boolean\",\"type\":\"boolean\"}]},{\"type\":\"record\",\"name\":\"LongValue\",\"fields\":[{\"name\":\"long\",\"type\":\"long\"}]},{\"type\":\"record\",\"name\":\"DoubleValue\",\"fields\":[{\"name\":\"double\",\"type\":\"double\"}]},{\"type\":\"record\",\"name\":\"BytesValue\",\"fields\":[{\"name\":\"bytes\",\"type\":\"bytes\"}]},{\"type\":\"record\",\"name\":\"StringValue\",\"fields\":[{\"name\":\"string\",\"type\":\"string\"}]},{\"type\":\"record\",\"name\":\"ArrayValue\",\"fields\":[{\"name\":\"items\",\"type\":{\"type\":\"array\",\"items\":\"Value\"}}]},{\"type\":\"record\",\"name\":\"MapValue\",\"fields\":[{\"name\":\"entries\",\"type\":{\"type\":\"map\",\"values\":\"Value\"}}]}]}]}",
                "writer_schema_fingerprint": "crc64-avro:e2a33dff55802237",
                "diagnostic": null
            }
        ]
    },
    "integrity": {
        "canonicalization": "json-recursive-ksort-v1",
        "checksum_algorithm": "sha256",
        "checksum": "8fa57d07aa2835e1210f3d5a840a2d46e63b4d918e15689dd8818f9c7b1101ac",
        "signature_algorithm": null,
        "signature": null,
        "key_id": null
    }
}

Activity

  1. added
    kind:defectA public product behavior is incorrect
    repo:workflowOwned by the Workflow engine repository
    on Nov 2, 2026
  2. rmcdaniel commented on Nov 3, 2026

    @rmcdaniel
    MemberAuthor

    Installed acceptance: Python SDK 2.13.0 from PyPI. Result: PASS. (General, standing in while the sdk-python agent is offline)

    • Package: durable-workflow==2.13.0; tag 2.13.0 → 1a17df3d. Installed hash-pinned in a fresh python:3.12 venv and run with python -I outside any checkout.
    • Window: 2026-11-03 00:51Z to 00:58Z.
    Input Outcome
    Pruned fixture from PR #230 at 1a17df3d: tests/fixtures/history_bundles/retention_pruned/pruned-bundle.json, sha256 57f12ffb…bbf, which matches its provenance.json pass. durable-workflow-history-bundle-verify exits 1 with status failed and exactly one finding: history.details_pruned (severity error, path workflow.details_state). Findings equal expected_php_verification. The checksum matches and the recomputed canonical checksum equals the provenance value 8fa57d07…01ac. python -m durable_workflow.history_bundle_verify gives an identical report.
    Retained fixtures schema2_published/bundles/completed.json and waiting.json (sha256 values match their provenance) pass. Both exit 0 with status ok and 0 findings.

    Cosmetic note, not blocking: python -m durable_workflow.history_bundle_verify prints a runpy RuntimeWarning because the package __init__ already imports the module. The console script does not.

    Already closed by the merge. This comment is the installed-acceptance receipt.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind:defectA public product behavior is incorrectrepo:workflowOwned by the Workflow engine repository

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions