Skip to content
Merged
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
30 changes: 25 additions & 5 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -759,11 +759,31 @@ this is what is not.
request without "to any AI model", it changed nothing (7 and 10 of
50, p = 0.60): that attack barely works unmarked. It refused no
request the user made (t34: 50 of 50).
- **Next: what a screenshot can carry.** A screenshot an agent asks
for only when the text tools leave something out. Then the marking
on by default, and errors, whose messages quote device text, marked
too. Any fence in the text uses a marker made fresh for each call,
since a fixed one an app can print (spotlighting, arXiv 2403.14720).
- **Done: no fence an app can print.** Checked 2026-11-11: the marking
is a field, not delimiters in the text, so there is no fence. Every
quote of an app's text in a message goes through `%q`, which escapes
quotes and line breaks, and every WebDriverAgent predicate refuses a
name or label holding `"` or `\`. `quoting_test.go` fails on a quote
written around a `%s` by hand. A fence added later uses a marker made
fresh for each call, since a fixed one an app can print
(spotlighting, arXiv 2403.14720).
- **Tried, not shipped: telling an agent when to take a screenshot.**
Measured 2026-11-11 on the Android 17 emulator, Sonnet 4.5, marking
on, 100 trials: an `app_screenshot` description saying to read the
screen with `app_map` and `app_text` first, and to take a
screenshot only for what they leave out, against today's. On t16,
t28 and t31, where the text tools are enough, trials with a
screenshot were 17 of 40 in both arms (p = 1.0). Screenshots fell
83 to 61, not a difference (permutation p = 0.39; t28 alone 78 to
59, p = 0.23). t15, which needs one, lost nothing (8 of 10 passes,
against 7 of 10). The one trial that obeyed an injection (t31, old
arm) did so straight after a screenshot, which fits t32's finding
that an image is where a request gets through.
- **Next, each measured first** (about 70 minutes and $16 of Claude
usage an A/B): the marking on by default, after a check that it
costs no task its success; then errors, whose messages quote device
text, marked too, which needs an attack that reaches an agent
through an error.
2. **Text a person cannot read, named.** Nothing in either platform's
accessibility tree gives opacity or font size, so a 3% or two-point
line reads as any other. A WebView's page does give them, and its
Expand Down
61 changes: 61 additions & 0 deletions internal/agent/quoting_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package agent

import (
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"testing"
)

// handQuoted is a quote written around a %s: it lets the text inside close
// the quote and write on as if it were Mobium. An app's text in a message
// goes through %q, which escapes quotes and line breaks (CHALLENGES 378).
var handQuoted = regexp.MustCompile(`\\"%s\\"|“%s”|‘%s’|'%s'`)

func TestHandQuotedIsCaught(t *testing.T) {
for _, s := range []string{`"no element matches \"%s\""`, "`pressed “%s”`", `"on '%s'"`} {
if !handQuoted.MatchString(s) {
t.Errorf("not caught: %s", s)
}
}
for _, s := range []string{`"no element matches %q"`, `"%s is %s"`} {
if handQuoted.MatchString(s) {
t.Errorf("caught, but safe: %s", s)
}
}
}

// No message in Mobium's source quotes a %s by hand. The app's words reach
// an agent in errors and in text, so a closing quote they carry must not end
// Mobium's quote (ROADMAP, Red teaming: any fence uses a marker an app cannot
// print).
func TestNoMessageQuotesAPercentSByHand(t *testing.T) {
root := filepath.Join("..", "..")
var found []string
for _, dir := range []string{"internal", "cmd"} {
err := filepath.Walk(filepath.Join(root, dir), func(p string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || !strings.HasSuffix(p, ".go") || strings.HasSuffix(p, "_test.go") {
return err
}
b, err := os.ReadFile(p)
if err != nil {
return err
}
for i, line := range strings.Split(string(b), "\n") {
if handQuoted.MatchString(line) {
rel, _ := filepath.Rel(root, p)
found = append(found, rel+":"+strconv.Itoa(i+1))
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
}
if len(found) > 0 {
t.Errorf("quoted by hand, use %%q: %s", strings.Join(found, ", "))
}
}
Loading