* 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RarRVX34ZW25TJejBZJ2Ui
---------
Co-authored-by: Claude <noreply@anthropic.com>