fix(verifier): a secure field's typed value never reaches the record

A folio login run wrote the account email and password in cleartext into
llm-calls.jsonl, 166 times in one run, beside screenshots of the same
screens. Three sites rendered it: the recent-action memory, the candidate
list, and the trace. One helper now covers all three so a fourth cannot
bypass it, and the driver still receives the real text.

Android redacts every typed value because it cannot tell a secure field
from any other. That asymmetry is deliberate and documented: safe by
default on the target that cannot tell.
This commit is contained in:
pj committed 2026-08-18 17:17:03 +05:30
1 parent b1e95739ad
commit 38d328df90
12 files changed
+418 -16

No files matched your search

+8 -4
View File
@@ -15,6 +15,7 @@ import (
"strings"
"time"
"github.com/priyanshujain/sanderling/internal/hierarchy"
"github.com/priyanshujain/sanderling/internal/llmclient"
"github.com/priyanshujain/sanderling/internal/trace"
"github.com/priyanshujain/sanderling/internal/verifier"
@@ -107,7 +108,7 @@ func (s *llmSource) NextAction(ctx context.Context, stepIndex int) (verifier.Act
if err == nil {
s.lastFromSetup = true
s.record(stepIndex, trace.LLMCall{Outcome: trace.LLMOutcomeSetupAction})
s.history.add(describeAction(action))
s.history.add(describeAction(action, s.verifier.Tree()))
return action, nil
}
if !errors.Is(err, verifier.ErrNoAction) {
@@ -129,7 +130,7 @@ func (s *llmSource) NextAction(ctx context.Context, stepIndex int) (verifier.Act
s.lastReasoning = selection.reasoning
s.lastChoice = selection.choice
s.lastChosenAction = selection.chosenAction
s.history.add(describeAction(selection.action))
s.history.add(describeAction(selection.action, s.verifier.Tree()))
return selection.action, nil
}
@@ -427,10 +428,13 @@ func parseChoice(content string) (choiceOutput, error) {
}
// describeAction renders a short action summary for the recent-action memory.
func describeAction(action verifier.Action) string {
// The tree is the one the action was chosen against, which is what says whether
// a typed value may be written into the memory at all.
func describeAction(action verifier.Action, tree *hierarchy.Tree) string {
switch action.Kind {
case verifier.ActionKindInputText:
return fmt.Sprintf("InputText %s = %q", actionTarget(action), action.Text)
return fmt.Sprintf("InputText %s = %q",
actionTarget(action), verifier.RecordedActionText(action, tree))
case verifier.ActionKindScroll:
// A builtin gesture carries endpoints rather than a selector, so name the
// container by where the drag starts; that is what tells two scrollable