Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .gitleaksignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 34 additions & 4 deletions packages/flow/openadapt_flow/privacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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|(?<!\w)(?:" + "|".join(branches) + r")(?!\w)"
)

def _replace(match: re.Match[str]) -> 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(
Expand Down
27 changes: 27 additions & 0 deletions packages/flow/tests/test_task_placeholders.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down
Loading