From e2f1138b4d33617573295560bfec5069679b6d9f Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Sun, 9 Aug 2026 21:08:13 +0200 Subject: [PATCH 1/3] fix: keep qualified workflow rendering current --- openadapt_flow/qualification.py | 5 +++++ tests/test_qualification_project.py | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/openadapt_flow/qualification.py b/openadapt_flow/qualification.py index 201b5143..9a9c80d6 100644 --- a/openadapt_flow/qualification.py +++ b/openadapt_flow/qualification.py @@ -5478,6 +5478,11 @@ def save_qualified_workflow( raise QualificationError( "governed authorization template changed the bundle content digest" ) + workflow_py = Path(bundle_dir) / "workflow.py" + if workflow_py.is_file(): + from openadapt_flow.compiler.codegen import render_workflow_py + + workflow_py.write_text(render_workflow_py(workflow), encoding="utf-8") return path diff --git a/tests/test_qualification_project.py b/tests/test_qualification_project.py index cf7618a0..acd93a2d 100644 --- a/tests/test_qualification_project.py +++ b/tests/test_qualification_project.py @@ -2448,11 +2448,16 @@ def test_minimum_effect_tier_versions_round_trips_and_invalidates_certification( def test_full_campaign_certifies_through_existing_policy_and_round_trips( tmp_path: Path, ) -> None: + from openadapt_flow.compiler.codegen import render_workflow_py + workflow = _workflow() bundle = tmp_path / "bundle" (bundle / "templates").mkdir(parents=True) (bundle / "templates" / "save.png").write_bytes(_qualification_visual_fixture()[1]) workflow.save(bundle) + (bundle / "workflow.py").write_text( + render_workflow_py(workflow), encoding="utf-8" + ) workflow = Workflow.load(bundle) _configure(workflow, tier=VerificationTier.INDEPENDENT_SYSTEM) evidence_root = tmp_path / "evidence" @@ -2472,6 +2477,9 @@ def test_full_campaign_certifies_through_existing_policy_and_round_trips( save_qualified_workflow(workflow, bundle) loaded = Workflow.load(bundle) + assert (bundle / "workflow.py").read_text(encoding="utf-8") == ( + render_workflow_py(loaded) + ) assert loaded.qualification is not None assert loaded.qualification.schema_version == "openadapt.qualification-project/v1" assert loaded.qualification.last_certification is not None From 14fa70699355d9d580f2bf8c8c072af2cdb224c3 Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Sun, 9 Aug 2026 21:11:55 +0200 Subject: [PATCH 2/3] fix: preserve qualified machine contracts during scrubbing --- openadapt_flow/sanitized_artifact.py | 16 +++++++++++++++- tests/test_sanitized_artifact.py | 16 ++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/openadapt_flow/sanitized_artifact.py b/openadapt_flow/sanitized_artifact.py index 33a9f391..05e14090 100644 --- a/openadapt_flow/sanitized_artifact.py +++ b/openadapt_flow/sanitized_artifact.py @@ -310,9 +310,23 @@ def _bundle_string_is_structural(path: tuple[str | int, ...], value: str) -> boo key = path[-1] if path and isinstance(path[-1], str) else "" if key.endswith(("_sha256", "_digest", "_hash", "_version")): return True + if key == "sha256" and re.fullmatch(r"[0-9a-f]{64}", value): + return True if key.endswith("_at") and value: return True - if key in {"recording_id", "id"} and _OPAQUE_MACHINE_ID.fullmatch(value): + if key in {"recording_id", "id", "step_id"} and _OPAQUE_MACHINE_ID.fullmatch( + value + ): + return True + if key.endswith("_id") and re.fullmatch( + r"[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}", value, re.IGNORECASE + ): + return True + if key == "environment_observer_id" and re.fullmatch( + r"[A-Za-z0-9][A-Za-z0-9._:-]{0,127}", value + ): + return True + if key == "relation" and value in {"left_of", "right_of", "above", "below"}: return True if key in {"c", "r", "phash", "salt", "structured"} and re.fullmatch( r"[0-9a-f]{16,}", value diff --git a/tests/test_sanitized_artifact.py b/tests/test_sanitized_artifact.py index 4e6121b6..36b8f913 100644 --- a/tests/test_sanitized_artifact.py +++ b/tests/test_sanitized_artifact.py @@ -32,6 +32,7 @@ APPROVAL_NAME, MANIFEST_NAME, SanitizationError, + _bundle_string_is_structural, _review_content_length, _valid_review_host, add_manual_image_redaction, @@ -81,6 +82,21 @@ def scrub_image(self, image, fill_color=None): return image +@pytest.mark.parametrize( + ("path", "value"), + [ + (("qualification", "cases", 0, "evidence", 0, "sha256"), "a" * 64), + (("steps", 0, "id"), "step_000"), + (("qualification", "identity_policies", "step_000", "step_id"), "step_000"), + (("qualification", "project_id"), "f6d239d0-0b5e-440f-b794-bbe69cc01115"), + (("qualification", "environment", "environment_observer_id"), "backend:PlaywrightBackend"), + (("steps", 0, "anchor", "landmarks", 0, "relation"), "left_of"), + ], +) +def test_bundle_machine_contract_strings_are_not_sent_to_phi_ner(path, value): + assert _bundle_string_is_structural(path, value) + + @pytest.fixture(autouse=True) def _scrubber(tmp_path, monkeypatch): privacy.set_text_scrubber(FakeScrubber()) From ded68b4061fb50dc51523c206b68a80a9e720e22 Mon Sep 17 00:00:00 2001 From: Richard Abrich Date: Sun, 9 Aug 2026 21:17:08 +0200 Subject: [PATCH 3/3] style: format qualified bundle fixes --- openadapt_flow/sanitized_artifact.py | 4 +--- tests/test_qualification_project.py | 4 +--- tests/test_sanitized_artifact.py | 5 ++++- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/openadapt_flow/sanitized_artifact.py b/openadapt_flow/sanitized_artifact.py index 05e14090..73a08df4 100644 --- a/openadapt_flow/sanitized_artifact.py +++ b/openadapt_flow/sanitized_artifact.py @@ -314,9 +314,7 @@ def _bundle_string_is_structural(path: tuple[str | int, ...], value: str) -> boo return True if key.endswith("_at") and value: return True - if key in {"recording_id", "id", "step_id"} and _OPAQUE_MACHINE_ID.fullmatch( - value - ): + if key in {"recording_id", "id", "step_id"} and _OPAQUE_MACHINE_ID.fullmatch(value): return True if key.endswith("_id") and re.fullmatch( r"[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}", value, re.IGNORECASE diff --git a/tests/test_qualification_project.py b/tests/test_qualification_project.py index acd93a2d..d00e7d55 100644 --- a/tests/test_qualification_project.py +++ b/tests/test_qualification_project.py @@ -2455,9 +2455,7 @@ def test_full_campaign_certifies_through_existing_policy_and_round_trips( (bundle / "templates").mkdir(parents=True) (bundle / "templates" / "save.png").write_bytes(_qualification_visual_fixture()[1]) workflow.save(bundle) - (bundle / "workflow.py").write_text( - render_workflow_py(workflow), encoding="utf-8" - ) + (bundle / "workflow.py").write_text(render_workflow_py(workflow), encoding="utf-8") workflow = Workflow.load(bundle) _configure(workflow, tier=VerificationTier.INDEPENDENT_SYSTEM) evidence_root = tmp_path / "evidence" diff --git a/tests/test_sanitized_artifact.py b/tests/test_sanitized_artifact.py index 36b8f913..82fec3ac 100644 --- a/tests/test_sanitized_artifact.py +++ b/tests/test_sanitized_artifact.py @@ -89,7 +89,10 @@ def scrub_image(self, image, fill_color=None): (("steps", 0, "id"), "step_000"), (("qualification", "identity_policies", "step_000", "step_id"), "step_000"), (("qualification", "project_id"), "f6d239d0-0b5e-440f-b794-bbe69cc01115"), - (("qualification", "environment", "environment_observer_id"), "backend:PlaywrightBackend"), + ( + ("qualification", "environment", "environment_observer_id"), + "backend:PlaywrightBackend", + ), (("steps", 0, "anchor", "landmarks", 0, "relation"), "left_of"), ], )