You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 775eddd
Browse filesBrowse the repository at this point in the historyBrowse files
feat: session-scoped echo protection for parameterized recorded inputs (#2013)
* feat: session-scoped echo protection for parameterized recorded inputs
Extends ADR 0017's fill-step-scoped guarantee to the whole recording
session (#1398). After #1349, a later read-only action (`wait`, `is`,
`get`) can independently observe and record an app-rendered echo of an
already-parameterized `fill --record-as` value in its own result or
target-v1 identity evidence, re-leaking the literal even though the
originating fill was protected.
- SessionState gains a small, ephemeral, never-serialized
literal->placeholder registry populated only from explicit
`--record-as` pairs, owned by session-action-recorder.ts.
- Result/event payload fields get content-aware substring redaction
(reusing the fill boundary's recursive scrub) for every literal
registered so far in the session, longest-literal-first.
- target-v1/targets-v1 identity evidence is never silently
text-substituted while still claiming a trustworthy identity (replay
compares against the live tree, which re-renders the real value).
A landmark-mode (wait) echo is dropped to no annotation, exactly like
#1349's existing identity-empty case, so an echoing landmark can no
longer serve as an ADR 0016 destination guard. Action-mode evidence
(get/is/mutating actions) redacts the label and downgrades
verification to "unverifiable" instead, since ADR 0012/0016 forbid
dropping required identity evidence.
- Amends ADR 0017 (new mechanism), ADR 0012 (#1349/writer-invariant
cross-references), and ADR 0016 (destination guard cross-reference).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RarRVX34ZW25TJejBZJ2Ui
* fix: placeholder-safe single-pass multi-literal redaction
Addresses review feedback on #2013: sequential single-literal
replacement (register somethinglong -> ${ABC}, then ABC -> ${OTHER})
could rewrite a placeholder produced by an earlier pass, corrupting it
to ${${OTHER}}.
Replaces the per-pair sequential loop with one placeholder-safe
left-to-right multi-literal pass (parameterizeAgainstLiteralMap): it
never re-scans text it has already emitted, so no literal can be
matched inside another pair's placeholder token in either direction.
A registered literal is matched before checking for an existing
placeholder token, so a value that itself happens to look like
${SOMETHING} is still redacted correctly. The scan uses a sticky regex
instead of slicing per character, and literal pairs are sorted once
per payload/evidence walk instead of once per string leaf.
parameterizeRecordedFillPayload/parameterizeBackendOutput are
generalized to take injected leaf-transform/carries callbacks so the
single-pair fill-boundary path (with its existing whitespace-collapse
behavior) and the new multi-pair session-wide path share one
structural traversal.
Adds regression coverage for both result payloads and action-mode
target evidence, plus the placeholder-shaped-literal edge case.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RarRVX34ZW25TJejBZJ2Ui
* fix: unexport parameterizeAgainstLiteralMap (CI: fallow dead-code gate)
Only used internally within this file (by parameterizeRecordedResultEcho
and parameterizeTargetEvidenceEcho); the export had no consumer outside
the module, which the fallow audit correctly flags as dead code.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RarRVX34ZW25TJejBZJ2Ui
---------
Co-authored-by: Claude <[email protected]>
> **Status: accepted.** Amends the data-flow boundary above from fill-step-scoped to
92
+
> recording-session-scoped, per issue [#1398](https://github.com/callstack/agent-device/issues/1398).
93
+
94
+
The boundary above protects only the originating fill's own request/recording path. After
95
+
[#1349](https://github.com/callstack/agent-device/issues/1349), a later read-only action —
96
+
`wait`'s landmark-mode evidence, `is`, `get` — records `target-v1` identity evidence and a result
97
+
payload independently, computed from whatever the app renders at that later step. That capture has no
98
+
memory of an earlier fill's literal, so an app-rendered echo of it (the filled field's own displayed
99
+
value; a search result, validation message, or confirmation label containing it; a destination landmark
100
+
whose accessible label includes it) can re-enter session state and publication through a completely
101
+
different, unparameterized action — even though the originating fill was parameterized.
102
+
103
+
**The guarantee is now recording-session-scoped, not fill-step-scoped.** For the lifetime of one
104
+
recording session, no later recorded action's own result payload or `target-v1`/`targets-v1` identity
105
+
evidence may re-serialize an app-rendered echo of a literal the session already parameterized.
106
+
107
+
The mechanism is the smallest explicit, ephemeral state that can recognize a later echo:
108
+
`SessionState.recordedFillLiterals`, an in-memory `Map<literal, placeholder>` populated only by the SAME
109
+
(literal, placeholder) pair a `fill --record-as` entry already computes for its own boundary above — one
110
+
entry per parameterized fill, added only after that fill's own entry has been recorded. It is never
111
+
serialized (not to the script, the session event log, or diagnostics), has no read API beyond the
112
+
recorder that owns it, and disappears with the session.
113
+
114
+
This is not a reversal of the "no secret-to-name table" rule in the Mapping contract above. That rule
115
+
rejects retaining state to influence a *naming* decision — comparing a new `--record-as` value against
116
+
prior ones to infer or deduplicate a variable name. `recordedFillLiterals` never informs a naming
117
+
decision: every entry's placeholder is exactly the name the author already chose, and the map is
118
+
consulted only to recognize that same, already-named value reappearing in unrelated later evidence — a
119
+
redaction lookup, not a secret registry with a naming or comparison API.
120
+
121
+
**Two treatments, by data class:**
122
+
123
+
-**Result/event/backend-output fields** (display data, never compared at replay): content-aware
124
+
substring redaction, reusing the same recursive backend-output scrub the fill boundary already applies
125
+
to its own entry, generalized over every literal registered so far in the session. The substitution
126
+
itself is one placeholder-safe left-to-right pass over each string, not N sequential full-string passes
127
+
— a naive sequential pass over the same value can corrupt an EARLIER pair's just-inserted placeholder
128
+
when a LATER pair's literal happens to be a substring of it (register `somethinglong -> ${ABC}`, then
129
+
`ABC -> ${OTHER}`, and a second pass over a value already rewritten to `${ABC}` matches "ABC" *inside*
130
+
that token, producing `${${OTHER}}`). The single pass tries every registered literal longest-first at
131
+
each position — so one registered value that is a substring of another (a username that is a prefix of
132
+
a password) is never partially consumed by the shorter pair — and never re-scans text it has already
133
+
emitted, so no literal can ever be matched inside another pair's placeholder token in either direction.
134
+
-**`target-v1`/`targets-v1` identity evidence** (`label`, `ancestry[].label`, `scrollRegion.label` — the
135
+
fields replay's own classification compares): never silently text-substituted while still claiming a
136
+
trustworthy identity. Replay compares recorded identity against the *live* tree, which legitimately
137
+
re-renders the real value again at replay time; a placeholder written into a recorded identity field
138
+
could therefore never verify correctly. Instead:
139
+
-**Landmark mode (`wait`).** An echo is treated exactly like #1349's existing identity-empty case: no
140
+
annotation is recorded, and the wait keeps its selector-existence semantics. Because ADR 0016's
141
+
destination guard requires `verification: "verified"`, a landmark whose only identity is a
142
+
parameterized-value echo simply stops qualifying as a guard — `session save-script` refuses it with
143
+
the existing "record a selector-targeted wait on a labeled or id-bearing landmark" recovery,
144
+
reproducing the motivating scenario's real resolution (switching the guard to the stable `Apps`
145
+
landmark) as an enforced outcome rather than an authoring convention.
0 commit comments