Skip to content

fix(tool-server): keep Chromium fill within field limits and report dialogs and cross-origin frames - #1487

Closed
latekvo wants to merge 8 commits into
mainfrom
fix/fill-chromium-constraints
Closed

latekvo wants to merge 8 commits into
mainfrom
fix/fill-chromium-constraints

Conversation

@latekvo

@latekvo latekvo commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

fill on Chromium (#1444) forced text past field limits and answered verified: true, wiped a number input it could not fill, failed under a JS alert with the generic "restart the app" CDP timeout and then wrote the field when the alert closed, and told the caller to "tap the field first" for focus in a cross-origin frame, which loops.

Fix

  • Text the field cannot hold fails with FILL_UNSUPPORTED_FIELD before anything changes: over maxlength (only the input types it applies to; a textarea's CRLF counts as one), or text a number or email input's own value rules rewrite (checked on a detached XHTML input of the same type, also in an SVG or XML document).
  • The value-setter fallback runs only when the edit fired no input (watched in the window's capture phase; only a page listener added there earlier can hide it) and left the old text (a refused edit), never over the browser's or the page's own rewrite, even one equal to the old text.
  • The read-back expects CR and CRLF as the LF a textarea or editable element stores. Clearing a number input that shows unparsable text deletes it, and that text is never verified.
  • A no-op evaluation goes first. A page that does not answer in 2 s gets nothing that writes. These timeouts, and a CDP connect that times out, map to FILL_NOT_VERIFIED/timeout, as on iOS and Android.
  • In an SVG document, where execCommand throws, a text control gets the value setter.
  • A cross-origin or sandboxed frame, or an <embed>, is FILL_UNSUPPORTED_FIELD. A <frame> or same-origin <object> is followed like an <iframe>.
  • Tool description and a fill footnote in reference/tools.mdx list the cases. Android is unchanged.
Case (Electron 148 and Chrome 155, same result) Before After
maxlength=3 field, fill abcdef verified: true, field abcdef FILL_UNSUPPORTED_FIELD, field unchanged
number field 42, fill abc FILL_NOT_VERIFIED, field "" FILL_UNSUPPORTED_FIELD, field 42
number field 42 in an SVG or XML document, fill abc "could not run in the page", field 42 FILL_UNSUPPORTED_FIELD, field 42
email field, fill a@b.co FILL_NOT_VERIFIED, field a@b.co FILL_UNSUPPORTED_FIELD, field unchanged
email field, fill a@ąę.pl (typing gives a@xn--2da4b.pl) verified: true, field a@ąę.pl FILL_NOT_VERIFIED, field a@xn--2da4b.pl
same, retried on a@xn--2da4b.pl verified: true, field a@ąę.pl (invalid) FILL_NOT_VERIFIED, field unchanged
alert() open, fill dlg 10 s DEBUGGER_CDP_REQUEST_TIMEOUT; field dlg once the alert closes 2 s FILL_NOT_VERIFIED "changed nothing"; field stays ""
same, first Chromium call of the tool-server 60 s DEBUGGER_CDP_REQUEST_TIMEOUT 60 s (shared service build, #894) FILL_NOT_VERIFIED "changed nothing"
page's input handler opens an alert 10 s DEBUGGER_CDP_REQUEST_TIMEOUT 10 s FILL_NOT_VERIFIED "may still change when the page resumes"
focus in a cross-origin or sandboxed iframe FILL_NO_FOCUSED_FIELD FILL_UNSUPPORTED_FIELD
focus in a <frame> of a frameset, or a same-origin <object> FILL_NO_FOCUSED_FIELD verified: true
focus in an <embed> FILL_NO_FOCUSED_FIELD FILL_UNSUPPORTED_FIELD
textarea, fill a\r\nb FILL_NOT_VERIFIED, field a\nb verified: true, field a\nb
number field showing typed 1., fill "" verified: true, field still shows 1. verified: true, field empty
12 with a mask that stops input and keeps digits, fill 12a verified: true, field 12a FILL_NOT_VERIFIED, field 12
Repro and E2E (before = origin/main 91542bd, after = this branch)

Both bundles built from this worktree (npm run build -w @swmansion/argent), driven through argent run <tool> under a sandbox HOME. Every field read back over raw CDP, not argent. Error codes from the tool-server event log. Each build ran 2+ times on Electron 148 and on real Chrome 155 (port 9383): every run of a build matched its other runs, and Electron matched Chrome. The retry, <object> and first-call rows, and the controls below, were re-run on headless Chrome 155 after the follow-up commits.

argent run boot-device --electronAppPath <app> --electronPort 9381
argent run open-url --udid chromium-cdp-9381 --url http://127.0.0.1:9481/fix.html
argent run gesture-tap --udid chromium-cdp-9381 --x <Max3 x> --y <Max3 y>   # <input maxlength=3>
argent run fill --udid chromium-cdp-9381 --text abcdef
# raw CDP: document.getElementById('maxl').value

Unchanged controls (both builds): plain input, text at exactly maxlength, clearing a maxlength field, valid number, clearing a number, type=number maxlength=2 with 123 (maxlength does not apply), valid email, url with surrounding spaces (typed input keeps them), password at its limit, a page whose execCommand refuses (setter fallback still fills it), same-origin iframe, keyboard into the cross-origin frame.

Browser semantics checked first over raw CDP: maxlength counts UTF-16 units (😀😀 into 3 leaves 😀); select() and insertText work on type=number; number abc/+5/1,5 and email whitespace are rewritten typed and set alike, url whitespace only when set; an alert() blocks every Runtime.evaluate and a late Page.enable does not report the open dialog (Electron), hence the no-op probe instead of dialog events.

Tests and local CI
  • New tests fail with the fix reverted (fill-tool.test.ts + fill-chromium-script.test.ts, 104 tests). Mutations of each new rule (each type in the maxlength list, CRLF count and read-back, maxlength=0, single-address email, capture-phase listener, number badInput, <embed>, > vs >=, probe type/multiple and namespace, email probe, the frame's own window, <frame>, <object> and its contentWindow check, unchanged-text and input guards, listener removal, SVG fallback, connect and timeout mapping, no-op probe, rethrow) each fail at least one test.
  • tool-server suite 7924 passed, 1 skipped, 1 failed: e2e-tool-aliases (81 vs 83), which fails the same way locally on main and passes in CI; argent, argent-tools-client, registry suites and test:scripts pass.
  • npm run build, typecheck:tests, typecheck:scripts, prettier --check ., eslint (run per package), knip on an unbuilt tree, docs typecheck and docusaurus build, and the SpiderShield description gate (9.08 / 9.0) pass.
Left out

chromium-storage set/remove/clear (and the Chromium clipboard route) write through Runtime.evaluate the same way: under an open alert chromium-storage set fails after 10 s with DEBUGGER_CDP_REQUEST_TIMEOUT and the value lands once the alert closes (2/2 runs). That predates fill, has its own error contract and wants a CDP-layer fix, so it is not changed here (#1562). A dialog that is already open before the first Chromium call still waits out the 60 s service build every Chromium tool shares (#894, #1476).

…ialogs and cross-origin frames

- Refuse text over a maxlength, and text a number or email input would
  rewrite, with FILL_UNSUPPORTED_FIELD before touching the field. The
  value-setter fallback only runs when the page left the old text, so it no
  longer overrides the browser's own rewrite of an edit.
- Send a no-op first: a page blocked by a JavaScript dialog gets nothing that
  writes and fails with FILL_NOT_VERIFIED (timeout) instead of the generic
  CDP timeout; a fill that times out midway says the field may still change.
- Focus in a cross-origin or sandboxed frame is FILL_UNSUPPORTED_FIELD, not
  FILL_NO_FOCUSED_FIELD; a <frame> is followed like an <iframe>.
- Tool description and the tools reference list the new cases.
…ect> frames

- The value-setter fallback also requires that the edit fired no input
  event, so a browser rewrite that equals the old text (an IDN email on a
  retry) is no longer forced over.
- A textarea's maxlength check counts CRLF and CR as the one LF it stores.
- A same-origin <object> document is followed like an <iframe>; a
  cross-origin one is FILL_UNSUPPORTED_FIELD; an <object> with no document
  (an image) stays FILL_NO_FOCUSED_FIELD.
- The page-not-answering error also names a crashed page and says when to
  restart the app.
- Docs and the tool description name the number and email cases exactly.
- Tests pin search, url, tel and email in the maxlength type list.

@latekvo latekvo left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Diplomat, Opus 5.5]: Review of 4024a90: claims vs code, nearest twin, non-happy paths, inputs, reachability, an absence pass and 40 mutations. Every comment below was reproduced with headless Chrome 155 and the PR's tool-server (POST /tools/fill), with fields read back over raw CDP and compared against real typing (CDP Input.insertText) or main.

Comment thread packages/docs/docs/reference/tools.mdx Resolved

@latekvo latekvo left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Diplomat, Opus 5.5]: Second pass over c005501 (the follow-up commit): input guard, <object> frames, CRLF count, prose and 20 mutations, each checked against headless Chrome 155 through the tool-server. No higher-severity findings.

…G-hosted fields

- Building the CDP service under a JavaScript dialog times out on its
  read-only setup; fill now reports that as FILL_NOT_VERIFIED "changed
  nothing", like the no-op probe, instead of the generic CDP timeout.
- execCommand throws in an SVG document; a text control there falls back to
  the value setter instead of the generic "could not run" error.
- The CRLF count matches CRLF only (a lone CR keeps its length).
- Tests pin the unchanged-text guard, both sides of the CRLF count, and the
  SVG fallback for controls and contenteditables.

@latekvo latekvo left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Diplomat, Opus 5.5]: Review of a10c104 (connect mapping, SVG fallback, CRLF test), checked with headless Chrome 155 and the tool-server.

…romium fill

- Compare the read-back against the text with CR and CRLF stored as LF, as a
  textarea or editable element stores them, so such text verifies.
- Clear a number input that shows text it cannot parse, and do not verify it
  while it still does.
- Watch for the edit's input in the window's capture phase, so a page
  listener that stops it cannot let the setter override the page's rewrite.
- Refuse focus in an <embed>, which fill cannot reach, instead of asking to
  tap the field again.
- Tell the caller to ask the user to close a dialog, which argent cannot do,
  and note connection errors beside the fill codes in the docs.
- Pin maxlength 0, single-address email probing and the untouched selection
  of a refused fill in tests.

@latekvo latekvo left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Diplomat, Opus 5.5]: Review of cfff563: the six review moves, the absence pass and about 30 mutations, each checked with headless Chrome 155 through the PR's tool-server. Fields were read back over raw CDP and compared against real typing (CDP Input.insertText) and the merge-base build 91542bd. All comments are low severity. A focused field inside a closed shadow root gets the same looping "tap first" answer and cannot be reached from the page. It is outside this diff and filed as #1662.

Comment thread packages/docs/docs/reference/tools.mdx Resolved
…ocuments in Chromium fill

In an SVG or XML document createElement makes an element in no namespace,
so the probe never refused and the value setter fallback wiped a number
field or trimmed an email one. Also: restart advice when a page crashes
mid-fill, the frame-window test the input listener lacked, and docs and a
comment that overstated what fill covers.

@latekvo latekvo left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Diplomat, Opus 5.5]: Review of 1f17de3: the six review moves, an absence pass and about 60 mutations, each checked with headless Chrome 155 through the PR's tool-server (POST /tools/fill). Fields were read back over raw CDP and compared against real typing (CDP Input.insertText) and the merge-base build 0064663. Every row of the description's table was re-run and matches. All comments below are low severity. Two problems outside this diff are filed: #1746 (a trailing or double space in a contenteditable never verifies, since Chrome stores it as &nbsp;) and #1747 (fill "" reports an image-only contenteditable as cleared and leaves the image).

Comment thread packages/docs/docs/reference/tools.mdx Resolved
@latekvo

latekvo commented Nov 4, 2026

Copy link
Copy Markdown
Member Author

Folded into #1753 as bf68d0e, to review together with the other capture fixes.

@latekvo latekvo closed this Nov 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant