Repository navigation
fix(input): check that every MultiEdit text is a string before typing - #612
Merged
Merged
Conversation
MultiEdit's schema leaves the items of each locs and labels entry
untyped, so a client can send a number as a field's text, as in
[[450, 320, 30]]. multi_edit() checked only the points before it
started, and Desktop.type() used the text only after it had clicked
its field and cleared it. A number failed there with "TypeError:
argument of type 'int' is not a container or iterable": by then the
fields before it had been typed and its own field emptied, and the
error did not say which field it was about.
multi_edit() now checks every text in the loop that already checks
every point, before any click or key, and raises ValueError("Text for
the field at (100,240) must be a string, not 30"), so the tool result
is an error that names the field. A list of strings, which type()
typed item by item, is refused too. String texts are typed as before.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
MultiEdit takes
locs=[[x, y, text], ...]orlabels=[[label, text], ...]. Both parameters are declared aslist[list] | str | None, so the schema leaves the items of each entry untyped:{"anyOf": [{"items": {"items": {}, "type": "array"}, "type": "array"}, {"type": "string"}, {"type": "null"}]}A client can therefore send a number as a field's text, for example
[[450, 320, 30]]for a quantity or an age field. Type declarestext: str, so its schema asks for a string, and a number is refused before the tool runs ("Input should be a valid string"). MultiEdit passes the number on.Desktop.multi_edit()checks every point before it types anything (#593), but not the texts. Each field then goes throughDesktop.type(..., clear=True), which clicks the field, clears it (Home, Shift+End and Backspace in a single-line field), and only then uses the text:For a number this raises
TypeError: argument of type 'int' is not a container or iterable('float','bool'and'NoneType'for 4.5, true and null). By then the fields before it have been typed, and its own field has been emptied. The tool result is an error that does not say which field it is about, so the agent cannot tell that some fields have already changed.Reproduction
At 3416c76 on Windows 11 (build 22631) with Python 3.14.9, with mocks only. The
Desktopcame fromDesktop.__new__, and MultiEdit was registered on a stand-in MCP object, as intests/test_multi_tools.py.uia.Clickanduia.SendKeysrecorded each call instead of sending it, anduia.GetFocusedControlreturned a stand-in edit box whose text followed the keys, as intests/test_type_clear_readback.py. There were two fields: "Name" at (100,200), holdingBob, and "Quantity" at (100,240), holding3. Output ofMultiEdit(locs=[[100, 200, "Ann"], [100, 240, 5]])onmain:The same happens with
labels=[[0, "Ann"], [1, 5]]and withlocssent as a JSON string, since both end up inmulti_edit().Fix
desktop/service.py,multi_edit(): the loop that checks every point before any input now also checks that each text is astr. If one is not, it raisesValueError("Text for the field at (100,240) must be a string, not 5")before any click or key, so the tool result is an error that names the field. Labels are resolved to coordinates beforemulti_edit()runs, so the message names the field the same way forlocsandlabels.["3", "0"]typed30); it is now refused like the rest.With the change, the reproduction gives:
I did not make MultiEdit convert numbers with
str(): Type refuses a number fortext, so MultiEdit now does the same. If you would rather have MultiEdit type a number's digits (30for 30), the check can convert numbers instead of refusing them.Tests
New
tests/test_multi_edit_text.py, mocks only. A fixture replacesuia.Clickanduia.SendKeyswith recorders,uia.GetVirtualScreenRectwith one 1920x1080 monitor,uia.GetFocusedControlwithNoneandservice.sleepwith a no-op. TheDesktopcomes fromDesktop.__new__, with a last Snapshot that found two fields: label 0 at (100,200) and label 1 at (100,240).test_text_that_is_not_a_string_is_refused_before_any_input:multi_edit([[100, 200, "Ann"], [100, 240, text]])raisesValueErrornaming (100,240), and no click or key is sent, fortext30, 4.5, True and None. (4 cases)test_string_texts_are_typed_into_each_field: with string texts, each field is clicked, cleared and typed, in order, as onmain.test_multiedit_tool_refuses_a_number_before_any_input: the MultiEdit tool raises the sameValueErrorand sends nothing forlocsas a list,locsas a JSON string andlabels. (3 cases)I ran each file on its own, in the project's environment:
On
main, 7 of the 8 new tests fail with theTypeErrorabove ('int','float','bool'or'NoneType').test_string_texts_are_typed_into_each_fieldpasses on both. All 8 pass on this branch.These pass on
mainand on this branch with the same counts:tests/test_multi_tools.py(3 passed),tests/test_pointer_bounds.py(60) andtests/test_type_clear_readback.py(20). I did not run the whole suite.ruff checkis clean on both changed files.ruff format --checkis clean on the new test file.desktop/service.pyis not formatted onmaineither (one hunk, inget_active_window()); this change adds no hunk to itsruff format --diff.Not tested: a real MultiEdit call on a real form. Everything ran against mocks. To check it on a real machine, open a window with two text fields, for example Notepad's find and replace box (Ctrl+H), call Snapshot, then call MultiEdit with
labels=[[<first field>, "abc"], [<second field>, 5]], with a number as the second text. Onmain, the first field should readabc, the second should be empty, and the tool should return theTypeError. With this change, the tool should return "Text for the field at (x,y) must be a string, not 5", and neither field should change.