From 8f544cda9df6a1087b470a21396304b81ad34f27 Mon Sep 17 00:00:00 2001 From: Yunus Ardaguney Date: Tue, 10 Nov 2026 13:43:27 +0300 Subject: [PATCH 1/2] fix(flow): prevent task placeholder rewriting and pin short-value binding (#2130) Signed-off-by: Yunus Ardaguney --- packages/flow/openadapt_flow/privacy.py | 38 +++++++++++++++++-- packages/flow/tests/test_task_placeholders.py | 27 +++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/packages/flow/openadapt_flow/privacy.py b/packages/flow/openadapt_flow/privacy.py index 7014c1194..5ef3bb45c 100644 --- a/packages/flow/openadapt_flow/privacy.py +++ b/packages/flow/openadapt_flow/privacy.py @@ -591,13 +591,43 @@ def bind_text(text: Optional[str], bindings: "Mapping[str, str]") -> Optional[st """Replace every whole-word occurrence of a bound value with its placeholder. Longer values are replaced first, so "Jane Sample" is not split by a - bound "Jane". The text is otherwise unchanged (nothing is scrubbed). + bound "Jane". Existing placeholders and already-bound spans are preserved + in one pass, so a short value like "task" does not corrupt a placeholder + name. The text is otherwise unchanged (nothing is scrubbed). """ if not text or not bindings: return text - for name, value in sorted(bindings.items(), key=lambda kv: -len(kv[1])): - text = bound_pattern(value).sub(name.replace("\\", "\\\\"), text) - return text + import re + + sorted_items = sorted( + ((k, v) for k, v in bindings.items() if v and str(v).strip()), + key=lambda kv: -len(kv[1]), + ) + if not sorted_items: + return text + + val_to_name: dict[str, str] = {} + branches: list[str] = [] + for name, value in sorted_items: + norm = " ".join(value.split()) + if norm not in val_to_name: + val_to_name[norm] = name + branches.append(r"\s+".join(re.escape(w) for w in value.split())) + + if not branches: + return text + + pattern = re.compile( + r"\u27e6[^\u27e6\u27e7]*\u27e7|(? str: + matched = match.group(0) + if matched.startswith("\u27e6") and matched.endswith("\u27e7"): + return matched + return val_to_name.get(" ".join(matched.split()), matched) + + return pattern.sub(_replace, text) def label_regions( diff --git a/packages/flow/tests/test_task_placeholders.py b/packages/flow/tests/test_task_placeholders.py index c3438cb20..9ffa869e3 100644 --- a/packages/flow/tests/test_task_placeholders.py +++ b/packages/flow/tests/test_task_placeholders.py @@ -104,6 +104,33 @@ def test_short_values_bind_in_task_text_but_not_on_page_text() -> None: ) +def test_task_value_cannot_rewrite_another_placeholder() -> None: + # Issue #2130: a short value that is also a word inside a placeholder name + # must not match inside placeholders inserted in the same or earlier pass. + bindings = privacy.task_bindings(identity=["Jane Sample"], params={"kind": "task"}) + kind = privacy.placeholder("kind") + out = privacy.bind_text("Do the task for Jane Sample", bindings) + assert out == f"Do the {kind} for {NAME}" + assert f"\u27e6{kind}." not in (out or "") + + +def test_bind_text_preserves_preexisting_placeholders() -> None: + bindings = privacy.task_bindings(params={"kind": "task"}) + kind = privacy.placeholder("kind") + text = f"Do the task for {NAME}" + assert privacy.bind_text(text, bindings) == f"Do the {kind} for {NAME}" + + +def test_short_value_overbinding_in_task_text_is_pinned() -> None: + # Issue #2130: task_text binds every non-empty value, so a value under 3 + # characters (like "M") replaces whole-word occurrences in task text. + bindings = privacy.task_bindings(params={"sex": "M"}) + sex = privacy.placeholder("sex") + assert privacy.bind_text("sex M ... Mark M done", bindings) == ( + f"sex {sex} ... Mark {sex} done" + ) + + def test_inputs_are_bound_under_their_own_names() -> None: bindings = privacy.task_bindings( identity=IDENTITY, params={"note": "Follow up in two weeks"} From 54c33e27461111e64f603b8eb948db9910395848 Mon Sep 17 00:00:00 2001 From: Yunus Ardaguney Date: Tue, 10 Nov 2026 12:46:17 +0300 Subject: [PATCH 2/2] ci: allowlist bundle compatibility fixtures for commit b0fd15ee in .gitleaksignore --- .gitleaksignore | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/.gitleaksignore b/.gitleaksignore index 1c94fc1ee..071e70afc 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -103,6 +103,40 @@ e8ec96725c4451aa64cb97230c18d04b55d98503:packages/flow/tests/e2e/test_mcp_teach_ 8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.6.0/provenance.json:generic-api-key:554 8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.6.0/provenance.json:generic-api-key:630 8ccf2cf3360b7d7720af04c36eee4f8e756bdf6e:packages/flow/tests/fixtures/bundle_compatibility/2.6.0/provenance.json:generic-api-key:642 +# The same fixtures after PR #1928 was merged to main (commit b0fd15ee). +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.2.0/provenance.json:generic-api-key:66 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.2.0/provenance.json:generic-api-key:182 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.2.0/provenance.json:generic-api-key:193 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.2.0/provenance.json:generic-api-key:248 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.2.0/provenance.json:generic-api-key:290 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.2.0/provenance.json:generic-api-key:348 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.2.0/provenance.json:generic-api-key:397 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.2.0/provenance.json:generic-api-key:416 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.2.0/provenance.json:generic-api-key:428 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.5.0/provenance.json:generic-api-key:85 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.5.0/provenance.json:generic-api-key:155 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.5.0/provenance.json:generic-api-key:190 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.5.0/provenance.json:generic-api-key:275 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.5.0/provenance.json:generic-api-key:351 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.5.0/provenance.json:generic-api-key:414 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.5.0/provenance.json:generic-api-key:488 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.5.0/provenance.json:generic-api-key:508 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.5.0/provenance.json:generic-api-key:528 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.5.0/provenance.json:generic-api-key:570 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.5.0/provenance.json:generic-api-key:643 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.5.0/provenance.json:generic-api-key:655 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.6.0/provenance.json:generic-api-key:85 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.6.0/provenance.json:generic-api-key:132 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.6.0/provenance.json:generic-api-key:168 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.6.0/provenance.json:generic-api-key:254 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.6.0/provenance.json:generic-api-key:316 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.6.0/provenance.json:generic-api-key:379 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.6.0/provenance.json:generic-api-key:468 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.6.0/provenance.json:generic-api-key:490 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.6.0/provenance.json:generic-api-key:511 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.6.0/provenance.json:generic-api-key:554 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.6.0/provenance.json:generic-api-key:630 +b0fd15ee8584cc3b646ae9bab04b4f9acbc59292:packages/flow/tests/fixtures/bundle_compatibility/2.6.0/provenance.json:generic-api-key:642 # OpenAdapt#1872 network-save redaction tests (PR #2014, first commit): synthetic # credentials the redactor must withhold. Later commits assemble them at runtime. ccc7c617e5cd8082909b455ec6176a5e4c0dd4f0:packages/flow/tests/test_extension_network.py:generic-api-key:44