From b36e3f37072a8a190bd54626f560aa59479451bd Mon Sep 17 00:00:00 2001 From: zeroxjf Date: Mon, 18 May 2026 11:53:30 -0400 Subject: [PATCH 1/5] Harden computer server launch and auth defaults --- libs/cua-bench/pyproject.toml | 4 + libs/cua-bench/uv.lock | 415 +++++++----- libs/python/agent/pyproject.toml | 6 +- .../computer_server/__init__.py | 11 +- .../computer-server/computer_server/cli.py | 2 +- .../computer_server/handlers/factory.py | 33 +- .../computer_server/handlers/generic.py | 16 +- .../computer_server/handlers/macos.py | 29 +- .../computer-server/computer_server/main.py | 604 +++++++++++++----- .../computer-server/computer_server/server.py | 2 +- libs/python/computer-server/pyproject.toml | 2 +- .../tests/test_auth_availability.py | 157 ++++- .../tests/test_import_safety.py | 37 ++ .../tests/test_window_launch.py | 43 ++ libs/python/cua-auto/cua_auto/window.py | 20 +- .../cua-auto/tests/test_window_launch.py | 38 ++ libs/python/cua-cli/pyproject.toml | 4 +- libs/python/cua-cli/uv.lock | 146 +++-- libs/python/cua-sandbox/uv.lock | 54 +- pyproject.toml | 6 +- uv.lock | 518 ++++++--------- 21 files changed, 1358 insertions(+), 789 deletions(-) create mode 100644 libs/python/computer-server/tests/test_import_safety.py create mode 100644 libs/python/computer-server/tests/test_window_launch.py create mode 100644 libs/python/cua-auto/tests/test_window_launch.py diff --git a/libs/cua-bench/pyproject.toml b/libs/cua-bench/pyproject.toml index 8d6a7c3f42..eed0e3dd93 100644 --- a/libs/cua-bench/pyproject.toml +++ b/libs/cua-bench/pyproject.toml @@ -71,6 +71,7 @@ browser = [ # Agent support agents = [ "cua-agent>=0.8.0", + "litellm>=1.83.7,<1.84", "anthropic>=0.26.0", ] @@ -130,6 +131,9 @@ Issues = "https://github.com/trycua/cua/issues" cb = "cua_bench.cli.main:main" cua-bench = "cua_bench.cli.main:main" +[tool.uv.sources] +cua-agent = { path = "../python/agent", editable = true } + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" diff --git a/libs/cua-bench/uv.lock b/libs/cua-bench/uv.lock index d9ae911bce..e7f435b414 100644 --- a/libs/cua-bench/uv.lock +++ b/libs/cua-bench/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.12, <3.14" resolution-markers = [ "python_full_version >= '3.13' and sys_platform == 'linux'", @@ -46,7 +46,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.13.0" +version = "3.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -57,42 +57,42 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/62/f1/8515650ac3121a9e55c7b217c60e7fae3e0134b5acfe65691781b5356929/aiohttp-3.13.0.tar.gz", hash = "sha256:378dbc57dd8cf341ce243f13fa1fa5394d68e2e02c15cd5f28eae35a70ec7f67", size = 7832348, upload-time = "2025-10-06T19:58:48.089Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/95/7e8bdfa6e79099a086d59d42589492f1fe9d29aae3cefb58b676015ce278/aiohttp-3.13.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1c272a9a18a5ecc48a7101882230046b83023bb2a662050ecb9bfcb28d9ab53a", size = 735585, upload-time = "2025-10-06T19:55:43.401Z" }, - { url = "https://files.pythonhosted.org/packages/9f/20/2f1d3ee06ee94eafe516810705219bff234d09f135d6951661661d5595ae/aiohttp-3.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:97891a23d7fd4e1afe9c2f4473e04595e4acb18e4733b910b6577b74e7e21985", size = 490613, upload-time = "2025-10-06T19:55:45.237Z" }, - { url = "https://files.pythonhosted.org/packages/74/15/ab8600ef6dc1dcd599009a81acfed2ea407037e654d32e47e344e0b08c34/aiohttp-3.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:475bd56492ce5f4cffe32b5533c6533ee0c406d1d0e6924879f83adcf51da0ae", size = 489750, upload-time = "2025-10-06T19:55:46.937Z" }, - { url = "https://files.pythonhosted.org/packages/33/59/752640c2b86ca987fe5703a01733b00d375e6cd2392bc7574489934e64e5/aiohttp-3.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c32ada0abb4bc94c30be2b681c42f058ab104d048da6f0148280a51ce98add8c", size = 1736812, upload-time = "2025-10-06T19:55:48.917Z" }, - { url = "https://files.pythonhosted.org/packages/3d/c6/dd6b86ddb852a7fdbcdc7a45b6bdc80178aef713c08279afcaee7a5a9f07/aiohttp-3.13.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4af1f8877ca46ecdd0bc0d4a6b66d4b2bddc84a79e2e8366bc0d5308e76bceb8", size = 1698535, upload-time = "2025-10-06T19:55:50.75Z" }, - { url = "https://files.pythonhosted.org/packages/33/e2/27c92d205b9e8cee7661670e8e9f187931b71e26d42796b153d2a0ba6949/aiohttp-3.13.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e04ab827ec4f775817736b20cdc8350f40327f9b598dec4e18c9ffdcbea88a93", size = 1766573, upload-time = "2025-10-06T19:55:53.106Z" }, - { url = "https://files.pythonhosted.org/packages/df/6a/1fc1ad71d130a30f7a207d8d958a41224c29b834463b5185efb2dbff6ad4/aiohttp-3.13.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a6d9487b9471ec36b0faedf52228cd732e89be0a2bbd649af890b5e2ce422353", size = 1865229, upload-time = "2025-10-06T19:55:55.01Z" }, - { url = "https://files.pythonhosted.org/packages/14/51/d0c1701a79fcb0109cff5304da16226581569b89a282d8e7f1549a7e3ec0/aiohttp-3.13.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e66c57416352f36bf98f6641ddadd47c93740a22af7150d3e9a1ef6e983f9a8", size = 1750379, upload-time = "2025-10-06T19:55:57.219Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3d/2ec4b934f85856de1c0c18e90adc8902adadbfac2b3c0b831bfeb7214fc8/aiohttp-3.13.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:469167d5372f5bb3aedff4fc53035d593884fff2617a75317740e885acd48b04", size = 1560798, upload-time = "2025-10-06T19:55:58.888Z" }, - { url = "https://files.pythonhosted.org/packages/38/56/e23d9c3e13006e599fdce3851517c70279e177871e3e567d22cf3baf5d6c/aiohttp-3.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a9f3546b503975a69b547c9fd1582cad10ede1ce6f3e313a2f547c73a3d7814f", size = 1697552, upload-time = "2025-10-06T19:56:01.172Z" }, - { url = "https://files.pythonhosted.org/packages/56/cb/caa32c2ccaeca0a3dc39129079fd2ad02f9406c3a5f7924340435b87d4cd/aiohttp-3.13.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6b4174fcec98601f0cfdf308ee29a6ae53c55f14359e848dab4e94009112ee7d", size = 1718609, upload-time = "2025-10-06T19:56:03.102Z" }, - { url = "https://files.pythonhosted.org/packages/fb/c0/5911856fef9e40fd1ccbb8c54a90116875d5753a92c1cac66ce2059b390d/aiohttp-3.13.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a533873a7a4ec2270fb362ee5a0d3b98752e4e1dc9042b257cd54545a96bd8ed", size = 1735887, upload-time = "2025-10-06T19:56:04.841Z" }, - { url = "https://files.pythonhosted.org/packages/0e/48/8d6f4757a24c02f0a454c043556593a00645d10583859f7156db44d8b7d3/aiohttp-3.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ce887c5e54411d607ee0959cac15bb31d506d86a9bcaddf0b7e9d63325a7a802", size = 1553079, upload-time = "2025-10-06T19:56:07.197Z" }, - { url = "https://files.pythonhosted.org/packages/39/fa/e82c9445e40b50e46770702b5b6ca2f767966d53e1a5eef03583ceac6df6/aiohttp-3.13.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d871f6a30d43e32fc9252dc7b9febe1a042b3ff3908aa83868d7cf7c9579a59b", size = 1762750, upload-time = "2025-10-06T19:56:09.376Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e6/9d30554e7f1e700bfeae4ab6b153d5dc7441606a9ec5e929288fa93a1477/aiohttp-3.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:222c828243b4789d79a706a876910f656fad4381661691220ba57b2ab4547865", size = 1717461, upload-time = "2025-10-06T19:56:11.551Z" }, - { url = "https://files.pythonhosted.org/packages/1f/e5/29cca547990a59ea54f0674fc01de98519fc628cfceeab6175711750eca7/aiohttp-3.13.0-cp312-cp312-win32.whl", hash = "sha256:682d2e434ff2f1108314ff7f056ce44e457f12dbed0249b24e106e385cf154b9", size = 424633, upload-time = "2025-10-06T19:56:13.316Z" }, - { url = "https://files.pythonhosted.org/packages/8b/68/46dd042d7bc62eab30bafdb8569f55ef125c3a88bb174270324224f8df56/aiohttp-3.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:0a2be20eb23888df130214b91c262a90e2de1553d6fb7de9e9010cec994c0ff2", size = 451401, upload-time = "2025-10-06T19:56:15.188Z" }, - { url = "https://files.pythonhosted.org/packages/86/2c/ac53efdc9c10e41399acc2395af98f835b86d0141d5c3820857eb9f6a14a/aiohttp-3.13.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:00243e51f16f6ec0fb021659d4af92f675f3cf9f9b39efd142aa3ad641d8d1e6", size = 730090, upload-time = "2025-10-06T19:56:16.858Z" }, - { url = "https://files.pythonhosted.org/packages/13/18/1ac95683e1c1d48ef4503965c96f5401618a04c139edae12e200392daae8/aiohttp-3.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:059978d2fddc462e9211362cbc8446747ecd930537fa559d3d25c256f032ff54", size = 488041, upload-time = "2025-10-06T19:56:18.659Z" }, - { url = "https://files.pythonhosted.org/packages/fd/79/ef0d477c771a642d1a881b92d226314c43d3c74bc674c93e12e679397a97/aiohttp-3.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:564b36512a7da3b386143c611867e3f7cfb249300a1bf60889bd9985da67ab77", size = 486989, upload-time = "2025-10-06T19:56:20.371Z" }, - { url = "https://files.pythonhosted.org/packages/37/b4/0e440481a0e77a551d6c5dcab5d11f1ff6b2b2ddb8dedc24f54f5caad732/aiohttp-3.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4aa995b9156ae499393d949a456a7ab0b994a8241a96db73a3b73c7a090eff6a", size = 1718331, upload-time = "2025-10-06T19:56:22.188Z" }, - { url = "https://files.pythonhosted.org/packages/e6/59/76c421cc4a75bb1aceadb92f20ee6f05a990aa6960c64b59e8e0d340e3f5/aiohttp-3.13.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55ca0e95a3905f62f00900255ed807c580775174252999286f283e646d675a49", size = 1686263, upload-time = "2025-10-06T19:56:24.393Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ac/5095f12a79c7775f402cfc3e83651b6e0a92ade10ddf7f2c78c4fed79f71/aiohttp-3.13.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:49ce7525853a981fc35d380aa2353536a01a9ec1b30979ea4e35966316cace7e", size = 1754265, upload-time = "2025-10-06T19:56:26.365Z" }, - { url = "https://files.pythonhosted.org/packages/05/d7/a48e4989bd76cc70600c505bbdd0d90ca1ad7f9053eceeb9dbcf9345a9ec/aiohttp-3.13.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2117be9883501eaf95503bd313eb4c7a23d567edd44014ba15835a1e9ec6d852", size = 1856486, upload-time = "2025-10-06T19:56:28.438Z" }, - { url = "https://files.pythonhosted.org/packages/1e/02/45b388b49e37933f316e1fb39c0de6fb1d77384b0c8f4cf6af5f2cbe3ea6/aiohttp-3.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d169c47e40c911f728439da853b6fd06da83761012e6e76f11cb62cddae7282b", size = 1737545, upload-time = "2025-10-06T19:56:30.688Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a7/4fde058f1605c34a219348a83a99f14724cc64e68a42480fc03cf40f9ea3/aiohttp-3.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:703ad3f742fc81e543638a7bebddd35acadaa0004a5e00535e795f4b6f2c25ca", size = 1552958, upload-time = "2025-10-06T19:56:32.528Z" }, - { url = "https://files.pythonhosted.org/packages/d1/12/0bac4d29231981e3aa234e88d1931f6ba38135ff4c2cf3afbb7895527630/aiohttp-3.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5bf635c3476f4119b940cc8d94ad454cbe0c377e61b4527f0192aabeac1e9370", size = 1681166, upload-time = "2025-10-06T19:56:34.81Z" }, - { url = "https://files.pythonhosted.org/packages/71/95/b829eb5f8ac1ca1d8085bb8df614c8acf3ff32e23ad5ad1173c7c9761daa/aiohttp-3.13.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:cfe6285ef99e7ee51cef20609be2bc1dd0e8446462b71c9db8bb296ba632810a", size = 1710516, upload-time = "2025-10-06T19:56:36.787Z" }, - { url = "https://files.pythonhosted.org/packages/47/6d/15ccf4ef3c254d899f62580e0c7fc717014f4d14a3ac31771e505d2c736c/aiohttp-3.13.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:34d8af6391c5f2e69749d7f037b614b8c5c42093c251f336bdbfa4b03c57d6c4", size = 1731354, upload-time = "2025-10-06T19:56:38.659Z" }, - { url = "https://files.pythonhosted.org/packages/46/6a/8acf6c57e03b6fdcc8b4c06392e66abaff3213ea275e41db3edb20738d91/aiohttp-3.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:12f5d820fadc5848d4559ea838aef733cf37ed2a1103bba148ac2f5547c14c29", size = 1548040, upload-time = "2025-10-06T19:56:40.578Z" }, - { url = "https://files.pythonhosted.org/packages/75/7d/fbfd59ab2a83fe2578ce79ac3db49727b81e9f4c3376217ad09c03c6d279/aiohttp-3.13.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f1338b61ea66f4757a0544ed8a02ccbf60e38d9cfb3225888888dd4475ebb96", size = 1756031, upload-time = "2025-10-06T19:56:42.492Z" }, - { url = "https://files.pythonhosted.org/packages/99/e7/cc9f0fdf06cab3ca61e6b62bff9a4b978b8ca736e9d76ddf54365673ab19/aiohttp-3.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:582770f82513419512da096e8df21ca44f86a2e56e25dc93c5ab4df0fe065bf0", size = 1714933, upload-time = "2025-10-06T19:56:45.542Z" }, - { url = "https://files.pythonhosted.org/packages/db/43/7abbe1de94748a58a71881163ee280fd3217db36e8344d109f63638fe16a/aiohttp-3.13.0-cp313-cp313-win32.whl", hash = "sha256:3194b8cab8dbc882f37c13ef1262e0a3d62064fa97533d3aa124771f7bf1ecee", size = 423799, upload-time = "2025-10-06T19:56:47.779Z" }, - { url = "https://files.pythonhosted.org/packages/c9/58/afab7f2b9e7df88c995995172eb78cae8a3d5a62d5681abaade86b3f0089/aiohttp-3.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:7897298b3eedc790257fef8a6ec582ca04e9dbe568ba4a9a890913b925b8ea21", size = 450138, upload-time = "2025-10-06T19:56:49.49Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/45/4a/064321452809dae953c1ed6e017504e72551a26b6f5708a5a80e4bf556ff/aiohttp-3.13.4.tar.gz", hash = "sha256:d97a6d09c66087890c2ab5d49069e1e570583f7ac0314ecf98294c1b6aaebd38", size = 7859748, upload-time = "2026-03-28T17:19:40.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/bd/ede278648914cabbabfdf95e436679b5d4156e417896a9b9f4587169e376/aiohttp-3.13.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ee62d4471ce86b108b19c3364db4b91180d13fe3510144872d6bad5401957360", size = 752158, upload-time = "2026-03-28T17:16:06.901Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/581c053253c07b480b03785196ca5335e3c606a37dc73e95f6527f1591fe/aiohttp-3.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c0fd8f41b54b58636402eb493afd512c23580456f022c1ba2db0f810c959ed0d", size = 501037, upload-time = "2026-03-28T17:16:08.82Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/a5ede193c08f13cc42c0a5b50d1e246ecee9115e4cf6e900d8dbd8fd6acb/aiohttp-3.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4baa48ce49efd82d6b1a0be12d6a36b35e5594d1dd42f8bfba96ea9f8678b88c", size = 501556, upload-time = "2026-03-28T17:16:10.63Z" }, + { url = "https://files.pythonhosted.org/packages/d6/10/88ff67cd48a6ec36335b63a640abe86135791544863e0cfe1f065d6cef7a/aiohttp-3.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d738ebab9f71ee652d9dbd0211057690022201b11197f9a7324fd4dba128aa97", size = 1757314, upload-time = "2026-03-28T17:16:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/8b/15/fdb90a5cf5a1f52845c276e76298c75fbbcc0ac2b4a86551906d54529965/aiohttp-3.13.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0ce692c3468fa831af7dceed52edf51ac348cebfc8d3feb935927b63bd3e8576", size = 1731819, upload-time = "2026-03-28T17:16:14.558Z" }, + { url = "https://files.pythonhosted.org/packages/ec/df/28146785a007f7820416be05d4f28cc207493efd1e8c6c1068e9bdc29198/aiohttp-3.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e08abcfe752a454d2cb89ff0c08f2d1ecd057ae3e8cc6d84638de853530ebab", size = 1793279, upload-time = "2026-03-28T17:16:16.594Z" }, + { url = "https://files.pythonhosted.org/packages/10/47/689c743abf62ea7a77774d5722f220e2c912a77d65d368b884d9779ef41b/aiohttp-3.13.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5977f701b3fff36367a11087f30ea73c212e686d41cd363c50c022d48b011d8d", size = 1891082, upload-time = "2026-03-28T17:16:18.71Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/f7f4f318c7e58c23b761c9b13b9a3c9b394e0f9d5d76fbc6622fa98509f6/aiohttp-3.13.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54203e10405c06f8b6020bd1e076ae0fe6c194adcee12a5a78af3ffa3c57025e", size = 1773938, upload-time = "2026-03-28T17:16:21.125Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/f207cb3121852c989586a6fc16ff854c4fcc8651b86c5d3bd1fc83057650/aiohttp-3.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:358a6af0145bc4dda037f13167bef3cce54b132087acc4c295c739d05d16b1c3", size = 1579548, upload-time = "2026-03-28T17:16:23.588Z" }, + { url = "https://files.pythonhosted.org/packages/6c/58/e1289661a32161e24c1fe479711d783067210d266842523752869cc1d9c2/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:898ea1850656d7d61832ef06aa9846ab3ddb1621b74f46de78fbc5e1a586ba83", size = 1714669, upload-time = "2026-03-28T17:16:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/96/0a/3e86d039438a74a86e6a948a9119b22540bae037d6ba317a042ae3c22711/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7bc30cceb710cf6a44e9617e43eebb6e3e43ad855a34da7b4b6a73537d8a6763", size = 1754175, upload-time = "2026-03-28T17:16:28.18Z" }, + { url = "https://files.pythonhosted.org/packages/f4/30/e717fc5df83133ba467a560b6d8ef20197037b4bb5d7075b90037de1018e/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4a31c0c587a8a038f19a4c7e60654a6c899c9de9174593a13e7cc6e15ff271f9", size = 1762049, upload-time = "2026-03-28T17:16:30.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/28/8f7a2d4492e336e40005151bdd94baf344880a4707573378579f833a64c1/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2062f675f3fe6e06d6113eb74a157fb9df58953ffed0cdb4182554b116545758", size = 1570861, upload-time = "2026-03-28T17:16:32.953Z" }, + { url = "https://files.pythonhosted.org/packages/78/45/12e1a3d0645968b1c38de4b23fdf270b8637735ea057d4f84482ff918ad9/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d1ba8afb847ff80626d5e408c1fdc99f942acc877d0702fe137015903a220a9", size = 1790003, upload-time = "2026-03-28T17:16:35.468Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/60374e18d590de16dcb39d6ff62f39c096c1b958e6f37727b5870026ea30/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b08149419994cdd4d5eecf7fd4bc5986b5a9380285bcd01ab4c0d6bfca47b79d", size = 1737289, upload-time = "2026-03-28T17:16:38.187Z" }, + { url = "https://files.pythonhosted.org/packages/02/bf/535e58d886cfbc40a8b0013c974afad24ef7632d645bca0b678b70033a60/aiohttp-3.13.4-cp312-cp312-win32.whl", hash = "sha256:fc432f6a2c4f720180959bc19aa37259651c1a4ed8af8afc84dd41c60f15f791", size = 434185, upload-time = "2026-03-28T17:16:40.735Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1a/d92e3325134ebfff6f4069f270d3aac770d63320bd1fcd0eca023e74d9a8/aiohttp-3.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:6148c9ae97a3e8bff9a1fc9c757fa164116f86c100468339730e717590a3fb77", size = 461285, upload-time = "2026-03-28T17:16:42.713Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ac/892f4162df9b115b4758d615f32ec63d00f3084c705ff5526630887b9b42/aiohttp-3.13.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:63dd5e5b1e43b8fb1e91b79b7ceba1feba588b317d1edff385084fcc7a0a4538", size = 745744, upload-time = "2026-03-28T17:16:44.67Z" }, + { url = "https://files.pythonhosted.org/packages/97/a9/c5b87e4443a2f0ea88cb3000c93a8fdad1ee63bffc9ded8d8c8e0d66efc6/aiohttp-3.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:746ac3cc00b5baea424dacddea3ec2c2702f9590de27d837aa67004db1eebc6e", size = 498178, upload-time = "2026-03-28T17:16:46.766Z" }, + { url = "https://files.pythonhosted.org/packages/94/42/07e1b543a61250783650df13da8ddcdc0d0a5538b2bd15cef6e042aefc61/aiohttp-3.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bda8f16ea99d6a6705e5946732e48487a448be874e54a4f73d514660ff7c05d3", size = 498331, upload-time = "2026-03-28T17:16:48.9Z" }, + { url = "https://files.pythonhosted.org/packages/20/d6/492f46bf0328534124772d0cf58570acae5b286ea25006900650f69dae0e/aiohttp-3.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b061e7b5f840391e3f64d0ddf672973e45c4cfff7a0feea425ea24e51530fc2", size = 1744414, upload-time = "2026-03-28T17:16:50.968Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/e02627b2683f68051246215d2d62b2d2f249ff7a285e7a858dc47d6b6a14/aiohttp-3.13.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b252e8d5cd66184b570d0d010de742736e8a4fab22c58299772b0c5a466d4b21", size = 1719226, upload-time = "2026-03-28T17:16:53.173Z" }, + { url = "https://files.pythonhosted.org/packages/7b/6c/5d0a3394dd2b9f9aeba6e1b6065d0439e4b75d41f1fb09a3ec010b43552b/aiohttp-3.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20af8aad61d1803ff11152a26146d8d81c266aa8c5aa9b4504432abb965c36a0", size = 1782110, upload-time = "2026-03-28T17:16:55.362Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2d/c20791e3437700a7441a7edfb59731150322424f5aadf635602d1d326101/aiohttp-3.13.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:13a5cc924b59859ad2adb1478e31f410a7ed46e92a2a619d6d1dd1a63c1a855e", size = 1884809, upload-time = "2026-03-28T17:16:57.734Z" }, + { url = "https://files.pythonhosted.org/packages/c8/94/d99dbfbd1924a87ef643833932eb2a3d9e5eee87656efea7d78058539eff/aiohttp-3.13.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:534913dfb0a644d537aebb4123e7d466d94e3be5549205e6a31f72368980a81a", size = 1764938, upload-time = "2026-03-28T17:17:00.221Z" }, + { url = "https://files.pythonhosted.org/packages/49/61/3ce326a1538781deb89f6cf5e094e2029cd308ed1e21b2ba2278b08426f6/aiohttp-3.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:320e40192a2dcc1cf4b5576936e9652981ab596bf81eb309535db7e2f5b5672f", size = 1570697, upload-time = "2026-03-28T17:17:02.985Z" }, + { url = "https://files.pythonhosted.org/packages/b6/77/4ab5a546857bb3028fbaf34d6eea180267bdab022ee8b1168b1fcde4bfdd/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9e587fcfce2bcf06526a43cb705bdee21ac089096f2e271d75de9c339db3100c", size = 1702258, upload-time = "2026-03-28T17:17:05.28Z" }, + { url = "https://files.pythonhosted.org/packages/79/63/d8f29021e39bc5af8e5d5e9da1b07976fb9846487a784e11e4f4eeda4666/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9eb9c2eea7278206b5c6c1441fdd9dc420c278ead3f3b2cc87f9b693698cc500", size = 1740287, upload-time = "2026-03-28T17:17:07.712Z" }, + { url = "https://files.pythonhosted.org/packages/55/3a/cbc6b3b124859a11bc8055d3682c26999b393531ef926754a3445b99dfef/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:29be00c51972b04bf9d5c8f2d7f7314f48f96070ca40a873a53056e652e805f7", size = 1753011, upload-time = "2026-03-28T17:17:10.053Z" }, + { url = "https://files.pythonhosted.org/packages/e0/30/836278675205d58c1368b21520eab9572457cf19afd23759216c04483048/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90c06228a6c3a7c9f776fe4fc0b7ff647fffd3bed93779a6913c804ae00c1073", size = 1566359, upload-time = "2026-03-28T17:17:12.433Z" }, + { url = "https://files.pythonhosted.org/packages/50/b4/8032cc9b82d17e4277704ba30509eaccb39329dc18d6a35f05e424439e32/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a533ec132f05fd9a1d959e7f34184cd7d5e8511584848dab85faefbaac573069", size = 1785537, upload-time = "2026-03-28T17:17:14.721Z" }, + { url = "https://files.pythonhosted.org/packages/17/7d/5873e98230bde59f493bf1f7c3e327486a4b5653fa401144704df5d00211/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1c946f10f413836f82ea4cfb90200d2a59578c549f00857e03111cf45ad01ca5", size = 1740752, upload-time = "2026-03-28T17:17:17.387Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f2/13e46e0df051494d7d3c68b7f72d071f48c384c12716fc294f75d5b1a064/aiohttp-3.13.4-cp313-cp313-win32.whl", hash = "sha256:48708e2706106da6967eff5908c78ca3943f005ed6bcb75da2a7e4da94ef8c70", size = 433187, upload-time = "2026-03-28T17:17:19.523Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c0/649856ee655a843c8f8664592cfccb73ac80ede6a8c8db33a25d810c12db/aiohttp-3.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:74a2eb058da44fa3a877a49e2095b591d4913308bb424c418b77beb160c55ce3", size = 459778, upload-time = "2026-03-28T17:17:21.964Z" }, ] [[package]] @@ -324,14 +324,14 @@ wheels = [ [[package]] name = "click" -version = "8.3.0" +version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, ] [[package]] @@ -469,30 +469,93 @@ wheels = [ [[package]] name = "cua-agent" -version = "0.6.2" -source = { registry = "https://pypi.org/simple" } +version = "0.8.2" +source = { editable = "../python/agent" } dependencies = [ { name = "aiohttp" }, { name = "anyio" }, { name = "asyncio" }, { name = "certifi" }, - { name = "cua-computer" }, { name = "cua-core" }, { name = "httpx" }, { name = "litellm" }, + { name = "pillow" }, { name = "pydantic" }, { name = "python-dotenv" }, { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b0/6c/df06ea0314a7eba2ae79e6d3cf9e42a72f8276b259e8722b9edfdd8bba2c/cua_agent-0.6.2.tar.gz", hash = "sha256:a1053e684d938488d75649f12448e74c88b7a029cd79d49de78931ef7098d393", size = 162689, upload-time = "2025-12-18T22:22:07.224Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/2d/6a9aeac58a625ea3d326acf23690e75ba49f4cde6c1457927efc3b94f789/cua_agent-0.6.2-py3-none-any.whl", hash = "sha256:40b0a9b03689f37d925176b4423aaaabc0d23d8273dbb21d0054b73c01cfb966", size = 202054, upload-time = "2025-12-18T22:22:04.92Z" }, -] + +[package.metadata] +requires-dist = [ + { name = "accelerate", marker = "extra == 'all'" }, + { name = "accelerate", marker = "extra == 'glm45v-hf'" }, + { name = "accelerate", marker = "extra == 'internvl-hf'" }, + { name = "accelerate", marker = "extra == 'moondream3'" }, + { name = "accelerate", marker = "extra == 'opencua-hf'" }, + { name = "accelerate", marker = "extra == 'uitars-hf'" }, + { name = "aiohttp", specifier = ">=3.9.3" }, + { name = "anyio", specifier = ">=4.4.1" }, + { name = "asyncio" }, + { name = "blobfile", marker = "extra == 'all'", specifier = ">=3.0.0" }, + { name = "blobfile", marker = "extra == 'opencua-hf'", specifier = ">=3.0.0" }, + { name = "certifi", specifier = ">=2024.2.2" }, + { name = "cua-computer", marker = "extra == 'computer'", editable = "../python/computer" }, + { name = "cua-core", editable = "../python/core" }, + { name = "cua-som", marker = "extra == 'omni'", editable = "../python/som" }, + { name = "einops", marker = "extra == 'all'" }, + { name = "einops", marker = "extra == 'internvl-hf'" }, + { name = "google-genai", marker = "extra == 'all'", specifier = ">=1.41.0" }, + { name = "google-genai", marker = "extra == 'cloud'", specifier = ">=1.41.0" }, + { name = "google-genai", marker = "extra == 'gemini'", specifier = ">=1.41.0" }, + { name = "gradio", marker = "extra == 'all'", specifier = ">=6.0.0" }, + { name = "gradio", marker = "extra == 'ui'", specifier = ">=6.0.0" }, + { name = "httpx", specifier = ">=0.27.0" }, + { name = "hud-python", marker = "extra == 'hud'", specifier = "==0.4.52" }, + { name = "litellm", specifier = ">=1.83.7,<1.84" }, + { name = "mlx-vlm", marker = "sys_platform == 'darwin' and extra == 'all'", specifier = ">=0.1.27" }, + { name = "mlx-vlm", marker = "sys_platform == 'darwin' and extra == 'uitars-mlx'", specifier = ">=0.1.27" }, + { name = "pillow", specifier = ">=10.0.0" }, + { name = "pillow", marker = "extra == 'all'", specifier = ">=10.0.0" }, + { name = "pillow", marker = "extra == 'cloud'", specifier = ">=10.0.0" }, + { name = "pillow", marker = "extra == 'qwen'", specifier = ">=10.0.0" }, + { name = "pydantic", specifier = ">=2.6.4" }, + { name = "python-dotenv", specifier = ">=1.0.1" }, + { name = "python-dotenv", marker = "extra == 'all'", specifier = ">=1.0.1" }, + { name = "python-dotenv", marker = "extra == 'ui'", specifier = ">=1.0.1" }, + { name = "qwen-agent", marker = "extra == 'all'" }, + { name = "qwen-agent", marker = "extra == 'cloud'" }, + { name = "qwen-agent", marker = "extra == 'qwen'" }, + { name = "qwen-vl-utils", marker = "extra == 'all'" }, + { name = "qwen-vl-utils", marker = "extra == 'cloud'" }, + { name = "qwen-vl-utils", marker = "extra == 'qwen'" }, + { name = "rich", specifier = ">=13.7.1" }, + { name = "tiktoken", marker = "extra == 'all'", specifier = ">=0.11.0" }, + { name = "tiktoken", marker = "extra == 'opencua-hf'", specifier = ">=0.11.0" }, + { name = "timm", marker = "extra == 'all'" }, + { name = "timm", marker = "extra == 'internvl-hf'" }, + { name = "torch", marker = "extra == 'all'" }, + { name = "torch", marker = "extra == 'glm45v-hf'" }, + { name = "torch", marker = "extra == 'internvl-hf'" }, + { name = "torch", marker = "extra == 'moondream3'" }, + { name = "torch", marker = "extra == 'opencua-hf'" }, + { name = "torch", marker = "extra == 'uitars-hf'" }, + { name = "transformers", marker = "extra == 'all'", specifier = ">=4.55.0" }, + { name = "transformers", marker = "extra == 'glm45v-hf'", specifier = ">=4.57.6,<5" }, + { name = "transformers", marker = "extra == 'internvl-hf'", specifier = ">=4.55.0" }, + { name = "transformers", marker = "extra == 'moondream3'", specifier = ">=4.55.0" }, + { name = "transformers", marker = "extra == 'opencua-hf'", specifier = ">=4.53.0" }, + { name = "transformers", marker = "extra == 'uitars-hf'", specifier = ">=4.54.0" }, + { name = "typing-extensions", specifier = ">=4.12.2" }, + { name = "yaspin", marker = "extra == 'all'", specifier = ">=3.1.0" }, + { name = "yaspin", marker = "extra == 'cli'", specifier = ">=3.1.0" }, + { name = "yaspin", marker = "extra == 'cloud'", specifier = ">=3.1.0" }, +] +provides-extras = ["computer", "openai", "anthropic", "qwen", "omni", "uitars", "uitars-mlx", "uitars-hf", "glm45v-hf", "opencua-hf", "internvl-hf", "moondream3", "ui", "cli", "hud", "gemini", "cloud", "all"] [[package]] name = "cua-bench" -version = "0.2.3" +version = "0.2.10" source = { editable = "." } dependencies = [ { name = "aiofiles" }, @@ -507,6 +570,7 @@ dependencies = [ { name = "matplotlib" }, { name = "numpy" }, { name = "pillow" }, + { name = "playwright" }, { name = "psutil" }, { name = "pyquery" }, { name = "python-dotenv" }, @@ -519,6 +583,7 @@ dependencies = [ agents = [ { name = "anthropic" }, { name = "cua-agent" }, + { name = "litellm" }, ] all = [ { name = "anthropic" }, @@ -528,6 +593,10 @@ all = [ { name = "google-cloud-logging" }, { name = "google-cloud-storage" }, { name = "grpcio" }, + { name = "litellm" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-sdk" }, { name = "playwright" }, ] browser = [ @@ -539,12 +608,20 @@ cloud = [ { name = "google-cloud-logging" }, { name = "google-cloud-storage" }, { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-sdk" }, ] dev = [ { name = "pytest" }, { name = "pytest-asyncio" }, { name = "ruff" }, ] +otel = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-sdk" }, +] rl = [ { name = "torch" }, { name = "torchvision" }, @@ -569,10 +646,11 @@ requires-dist = [ { name = "aiosqlite", marker = "extra == 'server'", specifier = ">=0.19.0" }, { name = "anthropic", marker = "extra == 'agents'", specifier = ">=0.26.0" }, { name = "beautifulsoup4", specifier = ">=4.14.2" }, - { name = "cua-agent", marker = "extra == 'agents'", specifier = ">=0.6.2" }, - { name = "cua-bench", extras = ["browser", "agents", "cloud"], marker = "extra == 'all'" }, + { name = "cua-agent", marker = "extra == 'agents'", editable = "../python/agent" }, + { name = "cua-bench", extras = ["browser", "agents", "cloud", "otel"], marker = "extra == 'all'" }, + { name = "cua-bench", extras = ["otel"], marker = "extra == 'cloud'" }, { name = "cua-computer", specifier = ">=0.4.19" }, - { name = "cua-core" }, + { name = "cua-core", specifier = ">=0.3.0,<0.4.0" }, { name = "datasets", specifier = ">=3.0.0" }, { name = "docker", specifier = ">=7.0.0" }, { name = "fastapi", marker = "extra == 'server'", specifier = ">=0.100.0" }, @@ -584,9 +662,14 @@ requires-dist = [ { name = "html5lib", specifier = ">=1.1" }, { name = "icoextract", marker = "extra == 'windows'", specifier = ">=0.2.0" }, { name = "jinja2", specifier = ">=3.1.0" }, + { name = "litellm", marker = "extra == 'agents'", specifier = ">=1.83.7,<1.84" }, { name = "matplotlib", specifier = ">=3.8.0" }, { name = "numpy", specifier = ">=2.0.0" }, + { name = "opentelemetry-api", marker = "extra == 'otel'", specifier = ">=1.20.0" }, + { name = "opentelemetry-exporter-otlp-proto-http", marker = "extra == 'otel'", specifier = ">=1.20.0" }, + { name = "opentelemetry-sdk", marker = "extra == 'otel'", specifier = ">=1.20.0" }, { name = "pillow", specifier = ">=11.0.0" }, + { name = "playwright", specifier = ">=1.55.0" }, { name = "playwright", marker = "extra == 'browser'", specifier = ">=1.40.0" }, { name = "psutil", specifier = ">=5.9.0" }, { name = "pypiwin32", marker = "extra == 'windows'", specifier = ">=223" }, @@ -605,11 +688,11 @@ requires-dist = [ { name = "verl", marker = "extra == 'rl'" }, { name = "websockets", specifier = ">=12.0" }, ] -provides-extras = ["all", "browser", "agents", "cloud", "server", "windows", "dev", "rl"] +provides-extras = ["all", "browser", "agents", "otel", "cloud", "server", "windows", "dev", "rl"] [[package]] name = "cua-computer" -version = "0.4.19" +version = "0.5.18" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -620,31 +703,40 @@ dependencies = [ { name = "websocket-client" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a4/ab/4834a349fb1b9f743b8cc41c1dda5cee3ce3930641f390d118c9d5198fae/cua_computer-0.4.19.tar.gz", hash = "sha256:e477c37ea42ccea1c3c0b815aa71a972c436509fbeef3888cd2e74d5f2ea14c1", size = 94383, upload-time = "2025-12-18T04:00:05.232Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/b1/0ad69b0fd25bb11b2f68c7db5481b8a020763a354f1f636698fee00f383d/cua_computer-0.5.18.tar.gz", hash = "sha256:4f15df2dffc60af7cf80e0954186b12301ee08fd12d6fdf8b531ec4c63650837", size = 102054, upload-time = "2026-04-15T21:22:03.49Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/92/5468cd82ad5cf604210ac6c99ac9a83b182feec415046e9a86069f171daf/cua_computer-0.4.19-py3-none-any.whl", hash = "sha256:62ed7f619a90c01f187a1eae6452ccbe7a6f91044e154aa69776cde76391bec8", size = 106403, upload-time = "2025-12-18T04:00:03.835Z" }, + { url = "https://files.pythonhosted.org/packages/ed/52/1d0ba187276259bd1306a4a568cf50fedc9b0ac48743a60629d6ba21ed81/cua_computer-0.5.18-py3-none-any.whl", hash = "sha256:afeff75460cacf4c2a112f9aeb972da3dfe134db9e916c71b8ae1466e67e0e85", size = 117684, upload-time = "2026-04-15T21:22:02.088Z" }, ] [[package]] name = "cua-core" -version = "0.1.9" -source = { registry = "https://pypi.org/simple" } +version = "0.3.1" +source = { editable = "../python/core" } dependencies = [ - { name = "httpx" }, { name = "posthog" }, - { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/ca/4d003a50f52d7130fdf0c78b16c80b32285cbb28fc8ded8683d71a2567c5/cua_core-0.1.9.tar.gz", hash = "sha256:d00da9d516b95f1b6c35c0bedf6d8310437f457d84406dbd3feeb3b5b89bd79c", size = 4451, upload-time = "2025-08-19T16:10:17.695Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/4e/ac18c3acbc79f2ebc0e7005951291528e704ee2359a41cbc5f32844730c0/cua_core-0.1.9-py3-none-any.whl", hash = "sha256:f587020380293215b96a457b0f80557252517c130d509fce27c3c25e68ccc933", size = 5154, upload-time = "2025-08-19T16:10:17.018Z" }, + +[package.metadata] +requires-dist = [ + { name = "opentelemetry-api", marker = "extra == 'otel'", specifier = ">=1.20.0" }, + { name = "opentelemetry-api", marker = "extra == 'telemetry'", specifier = ">=1.20.0" }, + { name = "opentelemetry-exporter-otlp-proto-http", marker = "extra == 'otel'", specifier = ">=1.20.0" }, + { name = "opentelemetry-exporter-otlp-proto-http", marker = "extra == 'telemetry'", specifier = ">=1.20.0" }, + { name = "opentelemetry-sdk", marker = "extra == 'otel'", specifier = ">=1.20.0" }, + { name = "opentelemetry-sdk", marker = "extra == 'telemetry'", specifier = ">=1.20.0" }, + { name = "posthog", specifier = ">=3.20.0" }, ] +provides-extras = ["otel", "telemetry"] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8.3.5" }] [[package]] name = "cuda-bindings" version = "12.9.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder" }, + { name = "cuda-pathfinder", marker = "sys_platform == 'linux'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/a9/c1/dabe88f52c3e3760d861401bb994df08f672ec893b8f7592dc91626adcf3/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fda147a344e8eaeca0c6ff113d2851ffca8f7dfc0a6c932374ee5c47caa649c8", size = 12151019, upload-time = "2025-10-21T14:51:43.167Z" }, @@ -1319,14 +1411,14 @@ wheels = [ [[package]] name = "importlib-metadata" -version = "8.7.0" +version = "8.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload-time = "2024-09-11T14:56:08.937Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload-time = "2024-09-11T14:56:07.019Z" }, ] [[package]] @@ -1387,7 +1479,7 @@ wheels = [ [[package]] name = "jsonschema" -version = "4.25.1" +version = "4.23.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -1395,9 +1487,9 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778, upload-time = "2024-07-08T18:40:05.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462, upload-time = "2024-07-08T18:40:00.165Z" }, ] [[package]] @@ -1460,7 +1552,7 @@ wheels = [ [[package]] name = "litellm" -version = "1.80.5" +version = "1.83.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -1476,9 +1568,9 @@ dependencies = [ { name = "tiktoken" }, { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/85/b8/357544534bef87dd2858432f3cbd3a0e5cc267caebca5ea86b03618786c5/litellm-1.80.5.tar.gz", hash = "sha256:922791c264845d9ed59e540c8fa74a74d237c1b209568a05ffeacd8b51770deb", size = 11885764, upload-time = "2025-11-22T23:41:42.25Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/7c/c095649380adc96c8630273c1768c2ad1e74aa2ee1dd8dd05d218a60569f/litellm-1.83.14.tar.gz", hash = "sha256:24aef9b47cdc424c833e32f3727f411741c690832cd1fe4405e0077144fe09c9", size = 14836599, upload-time = "2026-04-26T03:16:10.176Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/af/1d4693746ff9fbbe27a6e7d6394b801acf234e00c83f45ad1cb5bf2eaa6c/litellm-1.80.5-py3-none-any.whl", hash = "sha256:2ac5f4e88cd57ae056e00da8f872e1c2956653750929fba2fd9b007b400fdb77", size = 10671970, upload-time = "2025-11-22T23:41:39.923Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5c/1b5691575420135e90578543b2bf219497caa33cfd0af64cb38f30288450/litellm-1.83.14-py3-none-any.whl", hash = "sha256:92b11ba2a32cf80707ddf388d18526696c7999a21b418c5e3b6eda1243d2cfdb", size = 16457054, upload-time = "2026-04-26T03:16:05.72Z" }, ] [[package]] @@ -1837,7 +1929,7 @@ name = "nvidia-cudnn-cu12" version = "9.10.2.21" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, @@ -1848,7 +1940,7 @@ name = "nvidia-cufft-cu12" version = "11.3.3.83" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, @@ -1875,9 +1967,9 @@ name = "nvidia-cusolver-cu12" version = "11.7.3.90" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12" }, - { name = "nvidia-cusparse-cu12" }, - { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, @@ -1888,7 +1980,7 @@ name = "nvidia-cusparse-cu12" version = "12.5.8.93" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, @@ -1949,7 +2041,7 @@ wheels = [ [[package]] name = "openai" -version = "2.13.0" +version = "2.24.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1961,9 +2053,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0f/39/8e347e9fda125324d253084bb1b82407e5e3c7777a03dc398f79b2d95626/openai-2.13.0.tar.gz", hash = "sha256:9ff633b07a19469ec476b1e2b5b26c5ef700886524a7a72f65e6f0b5203142d5", size = 626583, upload-time = "2025-12-16T18:19:44.387Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/13/17e87641b89b74552ed408a92b231283786523edddc95f3545809fab673c/openai-2.24.0.tar.gz", hash = "sha256:1e5769f540dbd01cb33bc4716a23e67b9d695161a734aff9c5f925e2bf99a673", size = 658717, upload-time = "2026-02-24T20:02:07.958Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/d5/eb52edff49d3d5ea116e225538c118699ddeb7c29fa17ec28af14bc10033/openai-2.13.0-py3-none-any.whl", hash = "sha256:746521065fed68df2f9c2d85613bb50844343ea81f60009b60e6a600c9352c79", size = 1066837, upload-time = "2025-12-16T18:19:43.124Z" }, + { url = "https://files.pythonhosted.org/packages/c9/30/844dc675ee6902579b8eef01ed23917cc9319a1c9c0c14ec6e39340c96d0/openai-2.24.0-py3-none-any.whl", hash = "sha256:fed30480d7d6c884303287bde864980a4b137b60553ffbcf9ab4a233b7a73d94", size = 1120122, upload-time = "2026-02-24T20:02:05.669Z" }, ] [[package]] @@ -2002,6 +2094,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/48/28ed9e55dcf2f453128df738210a980e09f4e468a456fa3c763dbc8be70a/opentelemetry_api-1.37.0-py3-none-any.whl", hash = "sha256:accf2024d3e89faec14302213bc39550ec0f4095d1cf5ca688e1bfb1c8612f47", size = 65732, upload-time = "2025-09-11T10:28:41.826Z" }, ] +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/6c/10018cbcc1e6fff23aac67d7fd977c3d692dbe5f9ef9bb4db5c1268726cc/opentelemetry_exporter_otlp_proto_common-1.37.0.tar.gz", hash = "sha256:c87a1bdd9f41fdc408d9cc9367bb53f8d2602829659f2b90be9f9d79d0bfe62c", size = 20430, upload-time = "2025-09-11T10:29:03.605Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/13/b4ef09837409a777f3c0af2a5b4ba9b7af34872bc43609dda0c209e4060d/opentelemetry_exporter_otlp_proto_common-1.37.0-py3-none-any.whl", hash = "sha256:53038428449c559b0c564b8d718df3314da387109c4d36bd1b94c9a641b0292e", size = 18359, upload-time = "2025-09-11T10:28:44.939Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/e3/6e320aeb24f951449e73867e53c55542bebbaf24faeee7623ef677d66736/opentelemetry_exporter_otlp_proto_http-1.37.0.tar.gz", hash = "sha256:e52e8600f1720d6de298419a802108a8f5afa63c96809ff83becb03f874e44ac", size = 17281, upload-time = "2025-09-11T10:29:04.844Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/e9/70d74a664d83976556cec395d6bfedd9b85ec1498b778367d5f93e373397/opentelemetry_exporter_otlp_proto_http-1.37.0-py3-none-any.whl", hash = "sha256:54c42b39945a6cc9d9a2a33decb876eabb9547e0dcb49df090122773447f1aef", size = 19576, upload-time = "2025-09-11T10:28:46.726Z" }, +] + [[package]] name = "opentelemetry-exporter-prometheus" version = "0.58b0" @@ -2018,14 +2140,14 @@ wheels = [ [[package]] name = "opentelemetry-proto" -version = "1.39.1" +version = "1.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/49/1d/f25d76d8260c156c40c97c9ed4511ec0f9ce353f8108ca6e7561f82a06b2/opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8", size = 46152, upload-time = "2025-12-11T13:32:48.681Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/ea/a75f36b463a36f3c5a10c0b5292c58b31dbdde74f6f905d3d0ab2313987b/opentelemetry_proto-1.37.0.tar.gz", hash = "sha256:30f5c494faf66f77faeaefa35ed4443c5edb3b0aa46dad073ed7210e1a789538", size = 46151, upload-time = "2025-09-11T10:29:11.04Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007", size = 72535, upload-time = "2025-12-11T13:32:33.866Z" }, + { url = "https://files.pythonhosted.org/packages/c4/25/f89ea66c59bd7687e218361826c969443c4fa15dfe89733f3bf1e2a9e971/opentelemetry_proto-1.37.0-py3-none-any.whl", hash = "sha256:8ed8c066ae8828bbf0c39229979bdf583a126981142378a9cbe9d6fd5701c6e2", size = 72534, upload-time = "2025-09-11T10:28:56.831Z" }, ] [[package]] @@ -2460,7 +2582,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.0" +version = "2.12.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -2468,55 +2590,52 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/da/b8a7ee04378a53f6fefefc0c5e05570a3ebfdfa0523a878bcd3b475683ee/pydantic-2.12.0.tar.gz", hash = "sha256:c1a077e6270dbfb37bfd8b498b3981e2bb18f68103720e51fa6c306a5a9af563", size = 814760, upload-time = "2025-10-07T15:58:03.467Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/9d/d5c855424e2e5b6b626fbc6ec514d8e655a600377ce283008b115abb7445/pydantic-2.12.0-py3-none-any.whl", hash = "sha256:f6a1da352d42790537e95e83a8bdfb91c7efbae63ffd0b86fa823899e807116f", size = 459730, upload-time = "2025-10-07T15:58:01.576Z" }, + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] [[package]] name = "pydantic-core" -version = "2.41.1" +version = "2.41.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/14/12b4a0d2b0b10d8e1d9a24ad94e7bbb43335eaf29c0c4e57860e8a30734a/pydantic_core-2.41.1.tar.gz", hash = "sha256:1ad375859a6d8c356b7704ec0f547a58e82ee80bb41baa811ad710e124bc8f2f", size = 454870, upload-time = "2025-10-07T10:50:45.974Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/bc/5f520319ee1c9e25010412fac4154a72e0a40d0a19eb00281b1f200c0947/pydantic_core-2.41.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:db2f82c0ccbce8f021ad304ce35cbe02aa2f95f215cac388eed542b03b4d5eb4", size = 2099300, upload-time = "2025-10-06T21:10:30.463Z" }, - { url = "https://files.pythonhosted.org/packages/31/14/010cd64c5c3814fb6064786837ec12604be0dd46df3327cf8474e38abbbd/pydantic_core-2.41.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47694a31c710ced9205d5f1e7e8af3ca57cbb8a503d98cb9e33e27c97a501601", size = 1910179, upload-time = "2025-10-06T21:10:31.782Z" }, - { url = "https://files.pythonhosted.org/packages/8e/2e/23fc2a8a93efad52df302fdade0a60f471ecc0c7aac889801ac24b4c07d6/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e9decce94daf47baf9e9d392f5f2557e783085f7c5e522011545d9d6858e00", size = 1957225, upload-time = "2025-10-06T21:10:33.11Z" }, - { url = "https://files.pythonhosted.org/packages/b9/b6/6db08b2725b2432b9390844852e11d320281e5cea8a859c52c68001975fa/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab0adafdf2b89c8b84f847780a119437a0931eca469f7b44d356f2b426dd9741", size = 2053315, upload-time = "2025-10-06T21:10:34.87Z" }, - { url = "https://files.pythonhosted.org/packages/61/d9/4de44600f2d4514b44f3f3aeeda2e14931214b6b5bf52479339e801ce748/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5da98cc81873f39fd56882e1569c4677940fbc12bce6213fad1ead784192d7c8", size = 2224298, upload-time = "2025-10-06T21:10:36.233Z" }, - { url = "https://files.pythonhosted.org/packages/7a/ae/dbe51187a7f35fc21b283c5250571a94e36373eb557c1cba9f29a9806dcf/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:209910e88afb01fd0fd403947b809ba8dba0e08a095e1f703294fda0a8fdca51", size = 2351797, upload-time = "2025-10-06T21:10:37.601Z" }, - { url = "https://files.pythonhosted.org/packages/b5/a7/975585147457c2e9fb951c7c8dab56deeb6aa313f3aa72c2fc0df3f74a49/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365109d1165d78d98e33c5bfd815a9b5d7d070f578caefaabcc5771825b4ecb5", size = 2074921, upload-time = "2025-10-06T21:10:38.927Z" }, - { url = "https://files.pythonhosted.org/packages/62/37/ea94d1d0c01dec1b7d236c7cec9103baab0021f42500975de3d42522104b/pydantic_core-2.41.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:706abf21e60a2857acdb09502bc853ee5bce732955e7b723b10311114f033115", size = 2187767, upload-time = "2025-10-06T21:10:40.651Z" }, - { url = "https://files.pythonhosted.org/packages/d3/fe/694cf9fdd3a777a618c3afd210dba7b414cb8a72b1bd29b199c2e5765fee/pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bf0bd5417acf7f6a7ec3b53f2109f587be176cb35f9cf016da87e6017437a72d", size = 2136062, upload-time = "2025-10-06T21:10:42.09Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ae/174aeabd89916fbd2988cc37b81a59e1186e952afd2a7ed92018c22f31ca/pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2e71b1c6ceb9c78424ae9f63a07292fb769fb890a4e7efca5554c47f33a60ea5", size = 2317819, upload-time = "2025-10-06T21:10:43.974Z" }, - { url = "https://files.pythonhosted.org/packages/65/e8/e9aecafaebf53fc456314f72886068725d6fba66f11b013532dc21259343/pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:80745b9770b4a38c25015b517451c817799bfb9d6499b0d13d8227ec941cb513", size = 2312267, upload-time = "2025-10-06T21:10:45.34Z" }, - { url = "https://files.pythonhosted.org/packages/35/2f/1c2e71d2a052f9bb2f2df5a6a05464a0eb800f9e8d9dd800202fe31219e1/pydantic_core-2.41.1-cp312-cp312-win32.whl", hash = "sha256:83b64d70520e7890453f1aa21d66fda44e7b35f1cfea95adf7b4289a51e2b479", size = 1990927, upload-time = "2025-10-06T21:10:46.738Z" }, - { url = "https://files.pythonhosted.org/packages/b1/78/562998301ff2588b9c6dcc5cb21f52fa919d6e1decc75a35055feb973594/pydantic_core-2.41.1-cp312-cp312-win_amd64.whl", hash = "sha256:377defd66ee2003748ee93c52bcef2d14fde48fe28a0b156f88c3dbf9bc49a50", size = 2034703, upload-time = "2025-10-06T21:10:48.524Z" }, - { url = "https://files.pythonhosted.org/packages/b2/53/d95699ce5a5cdb44bb470bd818b848b9beadf51459fd4ea06667e8ede862/pydantic_core-2.41.1-cp312-cp312-win_arm64.whl", hash = "sha256:c95caff279d49c1d6cdfe2996e6c2ad712571d3b9caaa209a404426c326c4bde", size = 1972719, upload-time = "2025-10-06T21:10:50.256Z" }, - { url = "https://files.pythonhosted.org/packages/27/8a/6d54198536a90a37807d31a156642aae7a8e1263ed9fe6fc6245defe9332/pydantic_core-2.41.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70e790fce5f05204ef4403159857bfcd587779da78627b0babb3654f75361ebf", size = 2105825, upload-time = "2025-10-06T21:10:51.719Z" }, - { url = "https://files.pythonhosted.org/packages/4f/2e/4784fd7b22ac9c8439db25bf98ffed6853d01e7e560a346e8af821776ccc/pydantic_core-2.41.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9cebf1ca35f10930612d60bd0f78adfacee824c30a880e3534ba02c207cceceb", size = 1910126, upload-time = "2025-10-06T21:10:53.145Z" }, - { url = "https://files.pythonhosted.org/packages/f3/92/31eb0748059ba5bd0aa708fb4bab9fcb211461ddcf9e90702a6542f22d0d/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:170406a37a5bc82c22c3274616bf6f17cc7df9c4a0a0a50449e559cb755db669", size = 1961472, upload-time = "2025-10-06T21:10:55.754Z" }, - { url = "https://files.pythonhosted.org/packages/ab/91/946527792275b5c4c7dde4cfa3e81241bf6900e9fee74fb1ba43e0c0f1ab/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12d4257fc9187a0ccd41b8b327d6a4e57281ab75e11dda66a9148ef2e1fb712f", size = 2063230, upload-time = "2025-10-06T21:10:57.179Z" }, - { url = "https://files.pythonhosted.org/packages/31/5d/a35c5d7b414e5c0749f1d9f0d159ee2ef4bab313f499692896b918014ee3/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a75a33b4db105dd1c8d57839e17ee12db8d5ad18209e792fa325dbb4baeb00f4", size = 2229469, upload-time = "2025-10-06T21:10:59.409Z" }, - { url = "https://files.pythonhosted.org/packages/21/4d/8713737c689afa57ecfefe38db78259d4484c97aa494979e6a9d19662584/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08a589f850803a74e0fcb16a72081cafb0d72a3cdda500106942b07e76b7bf62", size = 2347986, upload-time = "2025-10-06T21:11:00.847Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ec/929f9a3a5ed5cda767081494bacd32f783e707a690ce6eeb5e0730ec4986/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a97939d6ea44763c456bd8a617ceada2c9b96bb5b8ab3dfa0d0827df7619014", size = 2072216, upload-time = "2025-10-06T21:11:02.43Z" }, - { url = "https://files.pythonhosted.org/packages/26/55/a33f459d4f9cc8786d9db42795dbecc84fa724b290d7d71ddc3d7155d46a/pydantic_core-2.41.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2ae423c65c556f09569524b80ffd11babff61f33055ef9773d7c9fabc11ed8d", size = 2193047, upload-time = "2025-10-06T21:11:03.787Z" }, - { url = "https://files.pythonhosted.org/packages/77/af/d5c6959f8b089f2185760a2779079e3c2c411bfc70ea6111f58367851629/pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:4dc703015fbf8764d6a8001c327a87f1823b7328d40b47ce6000c65918ad2b4f", size = 2140613, upload-time = "2025-10-06T21:11:05.607Z" }, - { url = "https://files.pythonhosted.org/packages/58/e5/2c19bd2a14bffe7fabcf00efbfbd3ac430aaec5271b504a938ff019ac7be/pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:968e4ffdfd35698a5fe659e5e44c508b53664870a8e61c8f9d24d3d145d30257", size = 2327641, upload-time = "2025-10-06T21:11:07.143Z" }, - { url = "https://files.pythonhosted.org/packages/93/ef/e0870ccda798c54e6b100aff3c4d49df5458fd64217e860cb9c3b0a403f4/pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:fff2b76c8e172d34771cd4d4f0ade08072385310f214f823b5a6ad4006890d32", size = 2318229, upload-time = "2025-10-06T21:11:08.73Z" }, - { url = "https://files.pythonhosted.org/packages/b1/4b/c3b991d95f5deb24d0bd52e47bcf716098fa1afe0ce2d4bd3125b38566ba/pydantic_core-2.41.1-cp313-cp313-win32.whl", hash = "sha256:a38a5263185407ceb599f2f035faf4589d57e73c7146d64f10577f6449e8171d", size = 1997911, upload-time = "2025-10-06T21:11:10.329Z" }, - { url = "https://files.pythonhosted.org/packages/a7/ce/5c316fd62e01f8d6be1b7ee6b54273214e871772997dc2c95e204997a055/pydantic_core-2.41.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42ae7fd6760782c975897e1fdc810f483b021b32245b0105d40f6e7a3803e4b", size = 2034301, upload-time = "2025-10-06T21:11:12.113Z" }, - { url = "https://files.pythonhosted.org/packages/29/41/902640cfd6a6523194123e2c3373c60f19006447f2fb06f76de4e8466c5b/pydantic_core-2.41.1-cp313-cp313-win_arm64.whl", hash = "sha256:ad4111acc63b7384e205c27a2f15e23ac0ee21a9d77ad6f2e9cb516ec90965fb", size = 1977238, upload-time = "2025-10-06T21:11:14.1Z" }, - { url = "https://files.pythonhosted.org/packages/04/04/28b040e88c1b89d851278478842f0bdf39c7a05da9e850333c6c8cbe7dfa/pydantic_core-2.41.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:440d0df7415b50084a4ba9d870480c16c5f67c0d1d4d5119e3f70925533a0edc", size = 1875626, upload-time = "2025-10-06T21:11:15.69Z" }, - { url = "https://files.pythonhosted.org/packages/d6/58/b41dd3087505220bb58bc81be8c3e8cbc037f5710cd3c838f44f90bdd704/pydantic_core-2.41.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71eaa38d342099405dae6484216dcf1e8e4b0bebd9b44a4e08c9b43db6a2ab67", size = 2045708, upload-time = "2025-10-06T21:11:17.258Z" }, - { url = "https://files.pythonhosted.org/packages/d7/b8/760f23754e40bf6c65b94a69b22c394c24058a0ef7e2aa471d2e39219c1a/pydantic_core-2.41.1-cp313-cp313t-win_amd64.whl", hash = "sha256:555ecf7e50f1161d3f693bc49f23c82cf6cdeafc71fa37a06120772a09a38795", size = 1997171, upload-time = "2025-10-06T21:11:18.822Z" }, - { url = "https://files.pythonhosted.org/packages/2b/3e/a51c5f5d37b9288ba30683d6e96f10fa8f1defad1623ff09f1020973b577/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b04fa9ed049461a7398138c604b00550bc89e3e1151d84b81ad6dc93e39c4c06", size = 2115344, upload-time = "2025-10-07T10:50:02.466Z" }, - { url = "https://files.pythonhosted.org/packages/5a/bd/389504c9e0600ef4502cd5238396b527afe6ef8981a6a15cd1814fc7b434/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:b3b7d9cfbfdc43c80a16638c6dc2768e3956e73031fca64e8e1a3ae744d1faeb", size = 1927994, upload-time = "2025-10-07T10:50:04.379Z" }, - { url = "https://files.pythonhosted.org/packages/ff/9c/5111c6b128861cb792a4c082677e90dac4f2e090bb2e2fe06aa5b2d39027/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eec83fc6abef04c7f9bec616e2d76ee9a6a4ae2a359b10c21d0f680e24a247ca", size = 1959394, upload-time = "2025-10-07T10:50:06.335Z" }, - { url = "https://files.pythonhosted.org/packages/14/3f/cfec8b9a0c48ce5d64409ec5e1903cb0b7363da38f14b41de2fcb3712700/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6771a2d9f83c4038dfad5970a3eef215940682b2175e32bcc817bdc639019b28", size = 2147365, upload-time = "2025-10-07T10:50:07.978Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, ] [[package]] @@ -2632,11 +2751,11 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.1.1" +version = "1.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] [[package]] @@ -3089,27 +3208,28 @@ wheels = [ [[package]] name = "tokenizers" -version = "0.22.1" +version = "0.22.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/46/fb6854cec3278fbfa4a75b50232c77622bc517ac886156e6afbfa4d8fc6e/tokenizers-0.22.1.tar.gz", hash = "sha256:61de6522785310a309b3407bac22d99c4db5dba349935e99e4d15ea2226af2d9", size = 363123, upload-time = "2025-09-19T09:49:23.424Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/33/f4b2d94ada7ab297328fc671fed209368ddb82f965ec2224eb1892674c3a/tokenizers-0.22.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:59fdb013df17455e5f950b4b834a7b3ee2e0271e6378ccb33aa74d178b513c73", size = 3069318, upload-time = "2025-09-19T09:49:11.848Z" }, - { url = "https://files.pythonhosted.org/packages/1c/58/2aa8c874d02b974990e89ff95826a4852a8b2a273c7d1b4411cdd45a4565/tokenizers-0.22.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8d4e484f7b0827021ac5f9f71d4794aaef62b979ab7608593da22b1d2e3c4edc", size = 2926478, upload-time = "2025-09-19T09:49:09.759Z" }, - { url = "https://files.pythonhosted.org/packages/1e/3b/55e64befa1e7bfea963cf4b787b2cea1011362c4193f5477047532ce127e/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d2962dd28bc67c1f205ab180578a78eef89ac60ca7ef7cbe9635a46a56422a", size = 3256994, upload-time = "2025-09-19T09:48:56.701Z" }, - { url = "https://files.pythonhosted.org/packages/71/0b/fbfecf42f67d9b7b80fde4aabb2b3110a97fac6585c9470b5bff103a80cb/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38201f15cdb1f8a6843e6563e6e79f4abd053394992b9bbdf5213ea3469b4ae7", size = 3153141, upload-time = "2025-09-19T09:48:59.749Z" }, - { url = "https://files.pythonhosted.org/packages/17/a9/b38f4e74e0817af8f8ef925507c63c6ae8171e3c4cb2d5d4624bf58fca69/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1cbe5454c9a15df1b3443c726063d930c16f047a3cc724b9e6e1a91140e5a21", size = 3508049, upload-time = "2025-09-19T09:49:05.868Z" }, - { url = "https://files.pythonhosted.org/packages/d2/48/dd2b3dac46bb9134a88e35d72e1aa4869579eacc1a27238f1577270773ff/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7d094ae6312d69cc2a872b54b91b309f4f6fbce871ef28eb27b52a98e4d0214", size = 3710730, upload-time = "2025-09-19T09:49:01.832Z" }, - { url = "https://files.pythonhosted.org/packages/93/0e/ccabc8d16ae4ba84a55d41345207c1e2ea88784651a5a487547d80851398/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afd7594a56656ace95cdd6df4cca2e4059d294c5cfb1679c57824b605556cb2f", size = 3412560, upload-time = "2025-09-19T09:49:03.867Z" }, - { url = "https://files.pythonhosted.org/packages/d0/c6/dc3a0db5a6766416c32c034286d7c2d406da1f498e4de04ab1b8959edd00/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ef6063d7a84994129732b47e7915e8710f27f99f3a3260b8a38fc7ccd083f4", size = 3250221, upload-time = "2025-09-19T09:49:07.664Z" }, - { url = "https://files.pythonhosted.org/packages/d7/a6/2c8486eef79671601ff57b093889a345dd3d576713ef047776015dc66de7/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ba0a64f450b9ef412c98f6bcd2a50c6df6e2443b560024a09fa6a03189726879", size = 9345569, upload-time = "2025-09-19T09:49:14.214Z" }, - { url = "https://files.pythonhosted.org/packages/6b/16/32ce667f14c35537f5f605fe9bea3e415ea1b0a646389d2295ec348d5657/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:331d6d149fa9c7d632cde4490fb8bbb12337fa3a0232e77892be656464f4b446", size = 9271599, upload-time = "2025-09-19T09:49:16.639Z" }, - { url = "https://files.pythonhosted.org/packages/51/7c/a5f7898a3f6baa3fc2685c705e04c98c1094c523051c805cdd9306b8f87e/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:607989f2ea68a46cb1dfbaf3e3aabdf3f21d8748312dbeb6263d1b3b66c5010a", size = 9533862, upload-time = "2025-09-19T09:49:19.146Z" }, - { url = "https://files.pythonhosted.org/packages/36/65/7e75caea90bc73c1dd8d40438adf1a7bc26af3b8d0a6705ea190462506e1/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a0f307d490295717726598ef6fa4f24af9d484809223bbc253b201c740a06390", size = 9681250, upload-time = "2025-09-19T09:49:21.501Z" }, - { url = "https://files.pythonhosted.org/packages/30/2c/959dddef581b46e6209da82df3b78471e96260e2bc463f89d23b1bf0e52a/tokenizers-0.22.1-cp39-abi3-win32.whl", hash = "sha256:b5120eed1442765cd90b903bb6cfef781fd8fe64e34ccaecbae4c619b7b12a82", size = 2472003, upload-time = "2025-09-19T09:49:27.089Z" }, - { url = "https://files.pythonhosted.org/packages/b3/46/e33a8c93907b631a99377ef4c5f817ab453d0b34f93529421f42ff559671/tokenizers-0.22.1-cp39-abi3-win_amd64.whl", hash = "sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138", size = 2674684, upload-time = "2025-09-19T09:49:24.953Z" }, + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, ] [[package]] @@ -3143,6 +3263,11 @@ dependencies = [ { name = "typing-extensions" }, ] wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/54/a2ba279afcca44bbd320d4e73675b282fcee3d81400ea1b53934efca6462/torch-2.10.0-2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:13ec4add8c3faaed8d13e0574f5cd4a323c11655546f91fbe6afa77b57423574", size = 79498202, upload-time = "2026-02-10T21:44:52.603Z" }, + { url = "https://files.pythonhosted.org/packages/ec/23/2c9fe0c9c27f7f6cb865abcea8a4568f29f00acaeadfc6a37f6801f84cb4/torch-2.10.0-2-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:e521c9f030a3774ed770a9c011751fb47c4d12029a3d6522116e48431f2ff89e", size = 79498254, upload-time = "2026-02-10T21:44:44.095Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7a/abada41517ce0011775f0f4eacc79659bc9bc6c361e6bfe6f7052a6b9363/torch-2.10.0-3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:98c01b8bb5e3240426dcde1446eed6f40c778091c8544767ef1168fc663a05a6", size = 915622781, upload-time = "2026-03-11T14:17:11.354Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c6/4dfe238342ffdcec5aef1c96c457548762d33c40b45a1ab7033bb26d2ff2/torch-2.10.0-3-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:80b1b5bfe38eb0e9f5ff09f206dcac0a87aadd084230d4a36eea5ec5232c115b", size = 915627275, upload-time = "2026-03-11T14:16:11.325Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/72bf18847f58f877a6a8acf60614b14935e2f156d942483af1ffc081aea0/torch-2.10.0-3-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:46b3574d93a2a8134b3f5475cfb98e2eb46771794c57015f6ad1fb795ec25e49", size = 915523474, upload-time = "2026-03-11T14:17:44.422Z" }, { url = "https://files.pythonhosted.org/packages/cc/af/758e242e9102e9988969b5e621d41f36b8f258bb4a099109b7a4b4b50ea4/torch-2.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5fd4117d89ffd47e3dcc71e71a22efac24828ad781c7e46aaaf56bf7f2796acf", size = 145996088, upload-time = "2026-01-21T16:24:44.171Z" }, { url = "https://files.pythonhosted.org/packages/23/8e/3c74db5e53bff7ed9e34c8123e6a8bfef718b2450c35eefab85bb4a7e270/torch-2.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:787124e7db3b379d4f1ed54dd12ae7c741c16a4d29b49c0226a89bea50923ffb", size = 915711952, upload-time = "2026-01-21T16:23:53.503Z" }, { url = "https://files.pythonhosted.org/packages/6e/01/624c4324ca01f66ae4c7cd1b74eb16fb52596dce66dbe51eff95ef9e7a4c/torch-2.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c66c61f44c5f903046cc696d088e21062644cbe541c7f1c4eaae88b2ad23547", size = 113757972, upload-time = "2026-01-21T16:24:39.516Z" }, diff --git a/libs/python/agent/pyproject.toml b/libs/python/agent/pyproject.toml index 58c7b64de6..1b3c29a302 100644 --- a/libs/python/agent/pyproject.toml +++ b/libs/python/agent/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "python-dotenv>=1.0.1", "cua-core>=0.3.0,<0.4.0", "certifi>=2024.2.2", - "litellm==1.80.0", + "litellm>=1.83.7,<1.84", "Pillow>=10.0.0" ] requires-python = ">=3.11,<3.14" @@ -52,7 +52,7 @@ uitars-hf = [ glm45v-hf = [ "accelerate", "torch", - "transformers-v4.55.0-GLM-4.5V-preview" + "transformers>=4.57.6,<5" ] opencua-hf = [ "accelerate", @@ -130,4 +130,4 @@ constraint-dependencies = ["fastrtc>0.43.0", "mlx-audio>0.2.3"] distribution = true [tool.pdm.build] -includes = ["cua_agent/"] \ No newline at end of file +includes = ["cua_agent/"] diff --git a/libs/python/computer-server/computer_server/__init__.py b/libs/python/computer-server/computer_server/__init__.py index ef28cbb104..6ffa80b59e 100644 --- a/libs/python/computer-server/computer_server/__init__.py +++ b/libs/python/computer-server/computer_server/__init__.py @@ -7,12 +7,17 @@ __version__: str = "0.1.0" -# Explicitly export Server for static type checkers -from .server import Server as Server # noqa: F401 - __all__ = ["Server", "run_cli"] +def __getattr__(name: str): + if name == "Server": + from .server import Server + + return Server + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + def run_cli() -> None: """Entry point for CLI""" from .cli import main diff --git a/libs/python/computer-server/computer_server/cli.py b/libs/python/computer-server/computer_server/cli.py index 1d2218cd2f..8d82730c85 100644 --- a/libs/python/computer-server/computer_server/cli.py +++ b/libs/python/computer-server/computer_server/cli.py @@ -30,7 +30,7 @@ def parse_args(args: Optional[List[str]] = None) -> argparse.Namespace: help="Auto-detect and log the actual screen resolution at startup", ) parser.add_argument( - "--host", default="0.0.0.0", help="Host to bind the server to (default: 0.0.0.0)" + "--host", default="127.0.0.1", help="Host to bind the server to (default: 127.0.0.1)" ) parser.add_argument( "--port", type=int, default=8000, help="Port to bind the server to (default: 8000)" diff --git a/libs/python/computer-server/computer_server/handlers/factory.py b/libs/python/computer-server/computer_server/handlers/factory.py index 00029fa10c..906fbdd093 100644 --- a/libs/python/computer-server/computer_server/handlers/factory.py +++ b/libs/python/computer-server/computer_server/handlers/factory.py @@ -17,23 +17,6 @@ OS_TYPE = get_current_os() -if OS_TYPE == "android": - from .android import ( - AndroidAccessibilityHandler, - AndroidAutomationHandler, - AndroidDesktopHandler, - AndroidFileHandler, - AndroidWindowHandler, - ) -elif OS_TYPE == "darwin": - from computer_server.diorama.macos import MacOSDioramaHandler - - from .macos import MacOSAccessibilityHandler, MacOSAutomationHandler -elif OS_TYPE == "linux": - from .linux import LinuxAccessibilityHandler, LinuxAutomationHandler -elif OS_TYPE == "windows": - from .windows import WindowsAccessibilityHandler, WindowsAutomationHandler - from .generic import GenericDesktopHandler, GenericFileHandler, GenericWindowHandler @@ -81,6 +64,14 @@ def create_handlers() -> Tuple[ GenericWindowHandler(), ) elif OS_TYPE == "android": + from .android import ( + AndroidAccessibilityHandler, + AndroidAutomationHandler, + AndroidDesktopHandler, + AndroidFileHandler, + AndroidWindowHandler, + ) + return ( AndroidAccessibilityHandler(), AndroidAutomationHandler(), @@ -90,6 +81,10 @@ def create_handlers() -> Tuple[ AndroidWindowHandler(), ) elif OS_TYPE == "darwin": + from computer_server.diorama.macos import MacOSDioramaHandler + + from .macos import MacOSAccessibilityHandler, MacOSAutomationHandler + return ( MacOSAccessibilityHandler(), MacOSAutomationHandler(), @@ -99,6 +94,8 @@ def create_handlers() -> Tuple[ GenericWindowHandler(), ) elif OS_TYPE == "linux": + from .linux import LinuxAccessibilityHandler, LinuxAutomationHandler + return ( LinuxAccessibilityHandler(), LinuxAutomationHandler(), @@ -108,6 +105,8 @@ def create_handlers() -> Tuple[ GenericWindowHandler(), ) elif OS_TYPE == "windows": + from .windows import WindowsAccessibilityHandler, WindowsAutomationHandler + return ( WindowsAccessibilityHandler(), WindowsAutomationHandler(), diff --git a/libs/python/computer-server/computer_server/handlers/generic.py b/libs/python/computer-server/computer_server/handlers/generic.py index af889f083e..1b00bed901 100644 --- a/libs/python/computer-server/computer_server/handlers/generic.py +++ b/libs/python/computer-server/computer_server/handlers/generic.py @@ -10,6 +10,7 @@ import base64 import os import platform +import shlex import subprocess import webbrowser from pathlib import Path @@ -24,6 +25,15 @@ pwc = None # type: ignore +def build_launch_argv(app: str, args: Optional[list[str]] = None) -> list[str]: + if args is not None: + return [app, *args] + argv = shlex.split(app) + if not argv: + raise ValueError("app must not be empty") + return argv + + def resolve_path(path: str) -> Path: """Resolve a path to its absolute path. Expand ~ to the user's home directory. @@ -108,11 +118,7 @@ async def open(self, target: str) -> Dict[str, Any]: async def launch(self, app: str, args: Optional[list[str]] = None) -> Dict[str, Any]: try: - if args: - proc = subprocess.Popen([app, *args]) - else: - # allow shell command like "libreoffice --writer" - proc = subprocess.Popen(app, shell=True) + proc = subprocess.Popen(build_launch_argv(app, args)) return {"success": True, "pid": proc.pid} except Exception as e: return {"success": False, "error": str(e)} diff --git a/libs/python/computer-server/computer_server/handlers/macos.py b/libs/python/computer-server/computer_server/handlers/macos.py index f1d339a22f..fdb97f5ab5 100644 --- a/libs/python/computer-server/computer_server/handlers/macos.py +++ b/libs/python/computer-server/computer_server/handlers/macos.py @@ -46,6 +46,7 @@ from pynput.keyboard import Key from pynput.mouse import Button from pynput.mouse import Controller as MouseController +from PIL import Image, ImageGrab from Quartz.CoreGraphics import * # type: ignore from Quartz.CoreGraphics import CGPoint, CGSize # type: ignore @@ -57,34 +58,6 @@ logger = logging.getLogger(__name__) -# Trigger accessibility permissions prompt on macOS -try: - # Source - https://stackoverflow.com/a/17134 - # Posted by Andreas - # Retrieved 2025-12-03, License - CC BY-SA 4.0 - # Attempt to create and post a mouse event to trigger the permissions prompt - # This will cause macOS to show "Python would like to control this computer using accessibility features" - current_pos = CGEventGetLocation(CGEventCreate(None)) - p = CGPoint() - p.x = current_pos.x - p.y = current_pos.y - - me = CGEventCreateMouseEvent(None, kCGEventMouseMoved, p, 0) - if me: - CGEventPost(kCGHIDEventTap, me) - CFRelease(me) -except Exception as e: - logger.debug(f"Failed to trigger accessibility permissions prompt: {e}") - -# Trigger screen recording prompt on macOS -try: - from PIL import Image, ImageGrab - - ImageGrab.grab() -except Exception as e: - logger.debug(f"Failed to trigger screenshot permissions prompt: {e}") - - # Fix: pynput's MouseController.position setter uses CGEventPost with # kCGEventMouseMoved internally, which silently fails in macOS VMs running # under Apple's Virtualization.framework (Tart, Lume, or any VZVirtualMachine- diff --git a/libs/python/computer-server/computer_server/main.py b/libs/python/computer-server/computer_server/main.py index 3715c8b10f..daccf56d9c 100644 --- a/libs/python/computer-server/computer_server/main.py +++ b/libs/python/computer-server/computer_server/main.py @@ -45,6 +45,21 @@ def _parse_bool_env(name: str) -> bool: return os.environ.get(name, "").lower().strip() in ("1", "true", "yes", "y", "on") +def _allow_unauthenticated_local() -> bool: + return _parse_bool_env("CUA_ALLOW_UNAUTHENTICATED_LOCAL") + + +def _run_command_enabled() -> bool: + return _parse_bool_env("CUA_ENABLE_RUN_COMMAND") + + +def _auth_required_error() -> str: + return ( + "Authentication required: set CONTAINER_NAME for API-key auth or " + "CUA_ALLOW_UNAUTHENTICATED_LOCAL=1 for explicit local development mode" + ) + + def _unavailable_status_code() -> Optional[int]: """Return the HTTP status code to use when CONTAINER_NAME is required but unset. @@ -178,6 +193,82 @@ async def _reject_websocket(cls, receive, send, status_code): app.add_middleware(UnavailableWithoutContainerMiddleware) + +class ControlSurfaceAuthMiddleware: + """Protect mounted control surfaces that do not use FastAPI dependencies.""" + + _PROTECTED_PREFIXES = ("/mcp",) + + def __init__(self, app): + self.app = app + + async def __call__(self, scope, receive, send): + scope_type = scope.get("type") + path = scope.get("path", "") + if scope_type in ("http", "websocket") and path.startswith(self._PROTECTED_PREFIXES): + ok, detail = await self._authenticate_scope(scope) + if not ok: + if scope_type == "http": + await self._reject_http(send, 401, detail) + else: + await self._reject_websocket(receive, send, 1008, detail) + return + await self.app(scope, receive, send) + + @staticmethod + async def _authenticate_scope(scope) -> tuple[bool, str]: + if _allow_unauthenticated_local(): + return True, "" + server_container_name = os.environ.get("CONTAINER_NAME") + if not server_container_name: + return False, _auth_required_error() + + headers = { + key.decode("latin1").lower(): value.decode("latin1") + for key, value in scope.get("headers", []) + } + container_name = headers.get("x-container-name") + api_key = headers.get("x-api-key") + if not container_name: + return False, "Container name required" + if not api_key: + return False, "API key required" + if not await auth_manager.auth(container_name, api_key): + return False, "Authentication failed" + return True, "" + + @staticmethod + async def _reject_http(send, status_code, detail): + body = json.dumps({"detail": detail}).encode() + await send( + { + "type": "http.response.start", + "status": status_code, + "headers": [ + (b"content-type", b"application/json"), + (b"content-length", str(len(body)).encode()), + ], + } + ) + await send({"type": "http.response.body", "body": body}) + + @staticmethod + async def _reject_websocket(receive, send, code, detail): + event = await receive() + if event.get("type") != "websocket.connect": + return + await send({"type": "websocket.accept"}) + await send( + { + "type": "websocket.send", + "text": json.dumps({"success": False, "error": detail, "status_code": 401}), + } + ) + await send({"type": "websocket.close", "code": code}) + + +app.add_middleware(ControlSurfaceAuthMiddleware) + # CORS configuration origins = ["*"] app.add_middleware( @@ -206,14 +297,15 @@ async def _reject_websocket(cls, receive, send, status_code): except Exception: package_version = "unknown" -( - accessibility_handler, - automation_handler, - diorama_handler, - file_handler, - desktop_handler, - window_handler, -) = HandlerFactory.create_handlers() +_handler_tuple = None +_command_handlers: Optional[Dict[str, Any]] = None + + +def _get_handler_tuple(): + global _handler_tuple + if _handler_tuple is None: + _handler_tuple = HandlerFactory.create_handlers() + return _handler_tuple # Helper function for direction-based scrolling @@ -226,6 +318,7 @@ async def _scroll_direction_handler(direction: str, clicks: int = 1) -> Dict[str clicks: Number of scroll clicks (default: 1) """ direction = direction.lower() + _, automation_handler, _, _, _, _ = _get_handler_tuple() if direction == "down": return await automation_handler.scroll_down(clicks) elif direction == "up": @@ -254,77 +347,271 @@ async def _scroll_direction_handler(direction: str, clicks: int = 1) -> Dict[str "rmdir": "delete_dir", } -handlers = { - "version": lambda: {"protocol": protocol_version, "package": package_version}, - # App-Use commands - "diorama_cmd": diorama_handler.diorama_cmd, - # Accessibility commands - "get_accessibility_tree": accessibility_handler.get_accessibility_tree, - "find_element": accessibility_handler.find_element, - # Shell commands - "run_command": automation_handler.run_command, - # File system commands - "file_exists": file_handler.file_exists, - "directory_exists": file_handler.directory_exists, - "list_dir": file_handler.list_dir, - "read_text": file_handler.read_text, - "write_text": file_handler.write_text, - "read_bytes": file_handler.read_bytes, - "write_bytes": file_handler.write_bytes, - "get_file_size": file_handler.get_file_size, - "delete_file": file_handler.delete_file, - "create_dir": file_handler.create_dir, - "delete_dir": file_handler.delete_dir, - # Desktop commands - "get_desktop_environment": desktop_handler.get_desktop_environment, - "set_wallpaper": desktop_handler.set_wallpaper, - # Window management - "open": window_handler.open, - "launch": window_handler.launch, - "get_current_window_id": window_handler.get_current_window_id, - "get_application_windows": window_handler.get_application_windows, - "get_window_name": window_handler.get_window_name, - "get_window_size": window_handler.get_window_size, - "get_window_position": window_handler.get_window_position, - "set_window_size": window_handler.set_window_size, - "set_window_position": window_handler.set_window_position, - "maximize_window": window_handler.maximize_window, - "minimize_window": window_handler.minimize_window, - "activate_window": window_handler.activate_window, - "close_window": window_handler.close_window, - # Mouse commands - "mouse_down": automation_handler.mouse_down, - "mouse_up": automation_handler.mouse_up, - "left_click": automation_handler.left_click, - "right_click": automation_handler.right_click, - "double_click": automation_handler.double_click, - "move_cursor": automation_handler.move_cursor, - "drag_to": automation_handler.drag_to, - "drag": automation_handler.drag, - # Keyboard commands - "key_down": automation_handler.key_down, - "key_up": automation_handler.key_up, - "type_text": automation_handler.type_text, - "press_key": automation_handler.press_key, - "hotkey": automation_handler.hotkey, - # Scrolling actions - "scroll": automation_handler.scroll, - "scroll_down": automation_handler.scroll_down, - "scroll_up": automation_handler.scroll_up, - "scroll_direction": _scroll_direction_handler, - # Screen actions - "screenshot": automation_handler.screenshot, - "get_cursor_position": automation_handler.get_cursor_position, - "get_screen_size": automation_handler.get_screen_size, - # Clipboard actions - "copy_to_clipboard": automation_handler.copy_to_clipboard, - "set_clipboard": automation_handler.set_clipboard, -} +_STATIC_COMMAND_NAMES = [ + "version", + "diorama_cmd", + "get_accessibility_tree", + "find_element", + "run_command", + "file_exists", + "directory_exists", + "list_dir", + "read_text", + "write_text", + "read_bytes", + "write_bytes", + "get_file_size", + "delete_file", + "create_dir", + "delete_dir", + "get_desktop_environment", + "set_wallpaper", + "open", + "launch", + "get_current_window_id", + "get_application_windows", + "get_window_name", + "get_window_size", + "get_window_position", + "set_window_size", + "set_window_position", + "maximize_window", + "minimize_window", + "activate_window", + "close_window", + "mouse_down", + "mouse_up", + "left_click", + "right_click", + "double_click", + "move_cursor", + "drag_to", + "drag", + "key_down", + "key_up", + "type_text", + "press_key", + "hotkey", + "scroll", + "scroll_down", + "scroll_up", + "scroll_direction", + "screenshot", + "get_cursor_position", + "get_screen_size", + "copy_to_clipboard", + "set_clipboard", +] +_DYNAMIC_COMMAND_NAMES = ["multitouch_gesture"] + + +def _version_handler() -> Dict[str, Any]: + return {"protocol": protocol_version, "package": package_version} + + +async def _multitouch_gesture_signature( + fingers: List[Dict[str, Any]], + screen_w: int, + screen_h: int, + duration_ms: int = 400, + steps: int = 0, +) -> Dict[str, Any]: + return {"success": False, "error": "signature only"} + + +def _get_command_handlers() -> Dict[str, Any]: + global _command_handlers + if _command_handlers is not None: + return _command_handlers + + ( + accessibility_handler, + automation_handler, + diorama_handler, + file_handler, + desktop_handler, + window_handler, + ) = _get_handler_tuple() + + handlers = { + "version": _version_handler, + # App-Use commands + "diorama_cmd": diorama_handler.diorama_cmd, + # Accessibility commands + "get_accessibility_tree": accessibility_handler.get_accessibility_tree, + "find_element": accessibility_handler.find_element, + # Shell commands + "run_command": automation_handler.run_command, + # File system commands + "file_exists": file_handler.file_exists, + "directory_exists": file_handler.directory_exists, + "list_dir": file_handler.list_dir, + "read_text": file_handler.read_text, + "write_text": file_handler.write_text, + "read_bytes": file_handler.read_bytes, + "write_bytes": file_handler.write_bytes, + "get_file_size": file_handler.get_file_size, + "delete_file": file_handler.delete_file, + "create_dir": file_handler.create_dir, + "delete_dir": file_handler.delete_dir, + # Desktop commands + "get_desktop_environment": desktop_handler.get_desktop_environment, + "set_wallpaper": desktop_handler.set_wallpaper, + # Window management + "open": window_handler.open, + "launch": window_handler.launch, + "get_current_window_id": window_handler.get_current_window_id, + "get_application_windows": window_handler.get_application_windows, + "get_window_name": window_handler.get_window_name, + "get_window_size": window_handler.get_window_size, + "get_window_position": window_handler.get_window_position, + "set_window_size": window_handler.set_window_size, + "set_window_position": window_handler.set_window_position, + "maximize_window": window_handler.maximize_window, + "minimize_window": window_handler.minimize_window, + "activate_window": window_handler.activate_window, + "close_window": window_handler.close_window, + # Mouse commands + "mouse_down": automation_handler.mouse_down, + "mouse_up": automation_handler.mouse_up, + "left_click": automation_handler.left_click, + "right_click": automation_handler.right_click, + "double_click": automation_handler.double_click, + "move_cursor": automation_handler.move_cursor, + "drag_to": automation_handler.drag_to, + "drag": automation_handler.drag, + # Keyboard commands + "key_down": automation_handler.key_down, + "key_up": automation_handler.key_up, + "type_text": automation_handler.type_text, + "press_key": automation_handler.press_key, + "hotkey": automation_handler.hotkey, + # Scrolling actions + "scroll": automation_handler.scroll, + "scroll_down": automation_handler.scroll_down, + "scroll_up": automation_handler.scroll_up, + "scroll_direction": _scroll_direction_handler, + # Screen actions + "screenshot": automation_handler.screenshot, + "get_cursor_position": automation_handler.get_cursor_position, + "get_screen_size": automation_handler.get_screen_size, + # Clipboard actions + "copy_to_clipboard": automation_handler.copy_to_clipboard, + "set_clipboard": automation_handler.set_clipboard, + } + + # Android-only command — registered only when the Android handler is active. + if hasattr(automation_handler, "multitouch_gesture"): + handlers["multitouch_gesture"] = automation_handler.multitouch_gesture + + _command_handlers = handlers + return handlers + + +def _has_command(command: str) -> bool: + if command in _STATIC_COMMAND_NAMES: + return True + if command in _DYNAMIC_COMMAND_NAMES: + if OS_TYPE != "android": + return False + return command in _get_command_handlers() + return False -# Android-only commands — registered only when the Android handler is active -# so non-Android server instances don't fail at startup with AttributeError. -if hasattr(automation_handler, "multitouch_gesture"): - handlers["multitouch_gesture"] = automation_handler.multitouch_gesture + +def _get_handler_func(command: str): + if command == "version": + return _version_handler + return _get_command_handlers()[command] + + +def _signature_params(handler_func) -> List[Dict[str, Any]]: + sig = inspect.signature(handler_func) + params = [] + for param in sig.parameters.values(): + if param.name == "self": + continue + params.append( + { + "name": param.name, + "required": param.default == inspect.Parameter.empty, + "default": None if param.default == inspect.Parameter.empty else param.default, + } + ) + return params + + +def _static_command_signature_sources() -> Dict[str, Any]: + from .diorama.base import BaseDioramaHandler + from .handlers.base import ( + BaseAccessibilityHandler, + BaseAutomationHandler, + BaseDesktopHandler, + ) + from .handlers.generic import GenericFileHandler, GenericWindowHandler + + return { + "version": _version_handler, + "diorama_cmd": BaseDioramaHandler.diorama_cmd, + "get_accessibility_tree": BaseAccessibilityHandler.get_accessibility_tree, + "find_element": BaseAccessibilityHandler.find_element, + "run_command": BaseAutomationHandler.run_command, + "file_exists": GenericFileHandler.file_exists, + "directory_exists": GenericFileHandler.directory_exists, + "list_dir": GenericFileHandler.list_dir, + "read_text": GenericFileHandler.read_text, + "write_text": GenericFileHandler.write_text, + "read_bytes": GenericFileHandler.read_bytes, + "write_bytes": GenericFileHandler.write_bytes, + "get_file_size": GenericFileHandler.get_file_size, + "delete_file": GenericFileHandler.delete_file, + "create_dir": GenericFileHandler.create_dir, + "delete_dir": GenericFileHandler.delete_dir, + "get_desktop_environment": BaseDesktopHandler.get_desktop_environment, + "set_wallpaper": BaseDesktopHandler.set_wallpaper, + "open": GenericWindowHandler.open, + "launch": GenericWindowHandler.launch, + "get_current_window_id": GenericWindowHandler.get_current_window_id, + "get_application_windows": GenericWindowHandler.get_application_windows, + "get_window_name": GenericWindowHandler.get_window_name, + "get_window_size": GenericWindowHandler.get_window_size, + "get_window_position": GenericWindowHandler.get_window_position, + "set_window_size": GenericWindowHandler.set_window_size, + "set_window_position": GenericWindowHandler.set_window_position, + "maximize_window": GenericWindowHandler.maximize_window, + "minimize_window": GenericWindowHandler.minimize_window, + "activate_window": GenericWindowHandler.activate_window, + "close_window": GenericWindowHandler.close_window, + "mouse_down": BaseAutomationHandler.mouse_down, + "mouse_up": BaseAutomationHandler.mouse_up, + "left_click": BaseAutomationHandler.left_click, + "right_click": BaseAutomationHandler.right_click, + "double_click": BaseAutomationHandler.double_click, + "move_cursor": BaseAutomationHandler.move_cursor, + "drag_to": BaseAutomationHandler.drag_to, + "drag": BaseAutomationHandler.drag, + "key_down": BaseAutomationHandler.key_down, + "key_up": BaseAutomationHandler.key_up, + "type_text": BaseAutomationHandler.type_text, + "press_key": BaseAutomationHandler.press_key, + "hotkey": BaseAutomationHandler.hotkey, + "scroll": BaseAutomationHandler.scroll, + "scroll_down": BaseAutomationHandler.scroll_down, + "scroll_up": BaseAutomationHandler.scroll_up, + "scroll_direction": _scroll_direction_handler, + "screenshot": BaseAutomationHandler.screenshot, + "get_cursor_position": BaseAutomationHandler.get_cursor_position, + "get_screen_size": BaseAutomationHandler.get_screen_size, + "copy_to_clipboard": BaseAutomationHandler.copy_to_clipboard, + "set_clipboard": BaseAutomationHandler.set_clipboard, + } + + +def _dynamic_command_signature_sources() -> Dict[str, Any]: + if OS_TYPE != "android": + return {} + return { + "multitouch_gesture": _multitouch_gesture_signature, + } class AuthenticationManager: @@ -344,17 +631,22 @@ async def auth(self, container_name: str, api_key: str) -> bool: DNS hiccup would flip the VM into a perpetually-401 state until the TTL expired. """ - # If no CONTAINER_NAME is set, always allow access (local development) - if not self.container_name: + expected_container_name = os.environ.get("CONTAINER_NAME") + + # If no CONTAINER_NAME is set, only allow explicit local development mode. + if not expected_container_name: + if _allow_unauthenticated_local(): + logger.info("Explicit unauthenticated local development mode enabled") + return True logger.info( - "No CONTAINER_NAME set in environment. Allowing access (local development mode)" + "No CONTAINER_NAME set and CUA_ALLOW_UNAUTHENTICATED_LOCAL is not enabled" ) - return True + return False # Layer 1: VM Identity Verification - if container_name != self.container_name: + if container_name != expected_container_name: logger.warning( - f"VM name mismatch. Expected: {self.container_name}, Got: {container_name}" + f"VM name mismatch. Expected: {expected_container_name}, Got: {container_name}" ) return False @@ -417,7 +709,11 @@ def _resolve_command(command: str) -> str: def _suggest_similar_commands(command: str) -> List[str]: """Suggest similar commands for typos.""" - all_commands = list(handlers.keys()) + list(COMMAND_ALIASES.keys()) + all_commands = list(_STATIC_COMMAND_NAMES) + all_commands.extend(_DYNAMIC_COMMAND_NAMES) + if _command_handlers is not None: + all_commands.extend(cmd for cmd in _command_handlers if cmd not in all_commands) + all_commands.extend(COMMAND_ALIASES.keys()) suggestions = [] command_lower = command.lower() for cmd in all_commands: @@ -442,17 +738,24 @@ async def status(): async def list_commands(): """List all available commands and their aliases.""" commands_info = {} - for cmd_name, handler_func in handlers.items(): + command_handlers = _command_handlers or _static_command_signature_sources() + if _command_handlers is None: + command_handlers = { + **command_handlers, + **_dynamic_command_signature_sources(), + } + command_names = list(_STATIC_COMMAND_NAMES) + if OS_TYPE == "android": + command_names.extend(cmd for cmd in _DYNAMIC_COMMAND_NAMES if cmd not in command_names) + if _command_handlers is not None: + command_names.extend(cmd for cmd in _command_handlers if cmd not in command_names) + for cmd_name in command_names: try: - sig = inspect.signature(handler_func) - params = [ - { - "name": p.name, - "required": p.default == inspect.Parameter.empty, - "default": None if p.default == inspect.Parameter.empty else p.default, - } - for p in sig.parameters.values() - ] + handler_func = command_handlers.get(cmd_name) + if handler_func is None: + params = [] + else: + params = _signature_params(handler_func) except (ValueError, TypeError): params = [] commands_info[cmd_name] = {"params": params} @@ -476,14 +779,20 @@ async def list_commands(): @app.websocket("/ws", name="websocket_endpoint") async def websocket_endpoint(websocket: WebSocket): - global handlers - # WebSocket message size is configured at the app or endpoint level, not on the instance await manager.connect(websocket) # Check if CONTAINER_NAME is set (indicating cloud provider) server_container_name = os.environ.get("CONTAINER_NAME") + if not server_container_name and not _allow_unauthenticated_local(): + await websocket.send_json( + {"success": False, "error": _auth_required_error(), "status_code": 401} + ) + await websocket.close() + manager.disconnect(websocket) + return + # If cloud provider, perform authentication handshake if server_container_name: try: @@ -559,7 +868,7 @@ async def websocket_endpoint(websocket: WebSocket): # Resolve command aliases command = _resolve_command(command) - if command not in handlers: + if not _has_command(command): suggestions = _suggest_similar_commands(command) error_msg = f"Unknown command: {command}" if suggestions: @@ -567,9 +876,18 @@ async def websocket_endpoint(websocket: WebSocket): await websocket.send_json({"success": False, "error": error_msg}) continue + if command == "run_command" and not _run_command_enabled(): + await websocket.send_json( + { + "success": False, + "error": "run_command is disabled; set CUA_ENABLE_RUN_COMMAND=1 to enable it", + } + ) + continue + try: # Filter params to only include those accepted by the handler function - handler_func = handlers[command] + handler_func = _get_handler_func(command) sig = inspect.signature(handler_func) filtered_params = {k: v for k, v in params.items() if k in sig.parameters} @@ -630,7 +948,7 @@ async def cmd_endpoint( "params": {...} } """ - global handlers + await _require_auth(container_name, api_key) # Parse request body try: @@ -646,29 +964,10 @@ async def cmd_endpoint( # Resolve command aliases command = _resolve_command(command) - # Check if CONTAINER_NAME is set (indicating cloud provider) server_container_name = os.environ.get("CONTAINER_NAME") - - # If cloud provider, perform authentication if server_container_name: - logger.info( - f"Cloud provider detected. CONTAINER_NAME: {server_container_name}. Performing authentication..." - ) - - # Validate required headers - if not container_name: - raise HTTPException(status_code=401, detail="Container name required") - - if not api_key: - raise HTTPException(status_code=401, detail="API key required") - - # Validate with AuthenticationManager - is_authenticated = await auth_manager.auth(container_name, api_key) - if not is_authenticated: - raise HTTPException(status_code=401, detail="Authentication failed") - # Emit vm_session_started event for funnel tracking - api_key_hash = hashlib.sha256(api_key.encode()).hexdigest() + api_key_hash = hashlib.sha256((api_key or "").encode()).hexdigest() record_event( "vm_session_started", { @@ -678,18 +977,24 @@ async def cmd_endpoint( }, ) - if command not in handlers: + if not _has_command(command): suggestions = _suggest_similar_commands(command) error_msg = f"Unknown command: {command}" if suggestions: error_msg += f". Did you mean: {', '.join(suggestions)}?" raise HTTPException(status_code=400, detail=error_msg) + if command == "run_command" and not _run_command_enabled(): + raise HTTPException( + status_code=403, + detail="run_command is disabled; set CUA_ENABLE_RUN_COMMAND=1 to enable it", + ) + async def generate_response(): """Generate streaming response for the command execution""" try: # Filter params to only include those accepted by the handler function - handler_func = handlers[command] + handler_func = _get_handler_func(command) sig = inspect.signature(handler_func) filtered_params = {k: v for k, v in params.items() if k in sig.parameters} @@ -726,10 +1031,12 @@ async def _require_auth( container_name: Optional[str], api_key: Optional[str], ) -> None: - """Raise HTTPException(401) when cloud auth is configured and credentials are invalid.""" + """Raise HTTPException(401) when auth is required and credentials are invalid.""" server_container_name = os.environ.get("CONTAINER_NAME") if not server_container_name: - return # local development — no auth required + if _allow_unauthenticated_local(): + return + raise HTTPException(status_code=401, detail=_auth_required_error()) if not container_name: raise HTTPException(status_code=401, detail="Container name required") if not api_key: @@ -757,6 +1064,11 @@ async def pty_create( Returns: ``{"pid": int, "cols": int, "rows": int}`` """ await _require_auth(container_name, api_key) + if not _run_command_enabled(): + raise HTTPException( + status_code=403, + detail="PTY command creation is disabled; set CUA_ENABLE_RUN_COMMAND=1 to enable it", + ) try: body = await request.json() except Exception: @@ -988,22 +1300,19 @@ async def agent_response_endpoint( if not HAS_AGENT: raise HTTPException(status_code=501, detail="ComputerAgent not available") - # Authenticate via AuthenticationManager if running in cloud (CONTAINER_NAME set) + # Authenticate unless this proxy is explicitly public or local unauthenticated + # mode has been intentionally enabled. container_name = os.environ.get("CONTAINER_NAME") - if container_name: - is_public = os.environ.get("CUA_ENABLE_PUBLIC_PROXY", "").lower().strip() in [ - "1", - "true", - "yes", - "y", - "on", - ] - if not is_public: + is_public = _parse_bool_env("CUA_ENABLE_PUBLIC_PROXY") + if not is_public: + if container_name: if not api_key: raise HTTPException(status_code=401, detail="Missing AGENT PROXY auth headers") ok = await auth_manager.auth(container_name, api_key) if not ok: raise HTTPException(status_code=401, detail="Unauthorized") + elif not _allow_unauthenticated_local(): + raise HTTPException(status_code=401, detail=_auth_required_error()) # Parse request body try: @@ -1050,6 +1359,8 @@ def _to_messages(data: Union[str, List[Dict[str, Any]]]) -> List[Dict[str, Any]] # and delegates to our existing automation/file/accessibility handlers. from cua_agent.computers import AsyncComputerHandler # runtime-checkable Protocol + accessibility_handler, automation_handler, _, file_handler, _, _ = _get_handler_tuple() + class DirectComputerInterface: """Interface wrapper providing BrowserTool compatibility. @@ -1294,6 +1605,8 @@ async def playwright_exec_endpoint( "params": {...} } """ + await _require_auth(container_name, api_key) + # Parse request body try: body = await request.json() @@ -1305,27 +1618,6 @@ async def playwright_exec_endpoint( if not command: raise HTTPException(status_code=400, detail="Command is required") - # Check if CONTAINER_NAME is set (indicating cloud provider) - server_container_name = os.environ.get("CONTAINER_NAME") - - # If cloud provider, perform authentication - if server_container_name: - logger.info( - f"Cloud provider detected. CONTAINER_NAME: {server_container_name}. Performing authentication..." - ) - - # Validate required headers - if not container_name: - raise HTTPException(status_code=401, detail="Container name required") - - if not api_key: - raise HTTPException(status_code=401, detail="API key required") - - # Validate with AuthenticationManager - is_authenticated = await auth_manager.auth(container_name, api_key) - if not is_authenticated: - raise HTTPException(status_code=401, detail="Authentication failed") - # Get browser manager and execute command try: browser_manager = get_browser_manager() @@ -1342,4 +1634,4 @@ async def playwright_exec_endpoint( if __name__ == "__main__": - uvicorn.run(app, host="0.0.0.0", port=8000) + uvicorn.run(app, host="127.0.0.1", port=8000) diff --git a/libs/python/computer-server/computer_server/server.py b/libs/python/computer-server/computer_server/server.py index a0e719ed5e..f9de93b1a0 100644 --- a/libs/python/computer-server/computer_server/server.py +++ b/libs/python/computer-server/computer_server/server.py @@ -35,7 +35,7 @@ class Server: def __init__( self, - host: str = "0.0.0.0", + host: str = "127.0.0.1", port: int = 8000, log_level: str = "info", ssl_keyfile: Optional[str] = None, diff --git a/libs/python/computer-server/pyproject.toml b/libs/python/computer-server/pyproject.toml index 8027060fb2..56d14cf12a 100644 --- a/libs/python/computer-server/pyproject.toml +++ b/libs/python/computer-server/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "websockets>=12.0", "pywinctl>=0.4.1", "playwright>=1.40.0", - "fastmcp>=2.0,<3", + "fastmcp>=3.2.0,<4", # PTY support (wraps cua-auto terminal engine) "cua-auto>=0.1.0", # OS-specific runtime deps diff --git a/libs/python/computer-server/tests/test_auth_availability.py b/libs/python/computer-server/tests/test_auth_availability.py index 5b93d90e4b..155938aad8 100644 --- a/libs/python/computer-server/tests/test_auth_availability.py +++ b/libs/python/computer-server/tests/test_auth_availability.py @@ -2,11 +2,14 @@ These tests verify two things: -1. **Backwards compat** — when neither ``CONTAINER_NAME`` nor - ``UNAVAILABLE_WITHOUT_CONTAINER_NAME`` is set, the server continues to - operate in local development mode (no auth required, requests succeed). +1. **Safer default** — when neither ``CONTAINER_NAME`` nor + ``CUA_ALLOW_UNAUTHENTICATED_LOCAL`` is set, control endpoints reject + unauthenticated requests. -2. **New behavior** — when ``UNAVAILABLE_WITHOUT_CONTAINER_NAME`` is truthy +2. **Explicit local mode** — when ``CUA_ALLOW_UNAUTHENTICATED_LOCAL`` is truthy, + local development requests continue to work. + +3. **Unavailable behavior** — when ``UNAVAILABLE_WITHOUT_CONTAINER_NAME`` is truthy and ``CONTAINER_NAME`` is unset, requests are rejected with the status code configured by ``UNAVAILABLE_WITHOUT_CONTAINER_NAME_RESPONSE_STATUS_CODE`` (default 503) @@ -19,6 +22,7 @@ from fastapi.testclient import TestClient try: + import computer_server.main as main from computer_server.main import _unavailable_status_code, app except Exception as import_error: # pragma: no cover - environment-dependent pytest.skip( @@ -32,6 +36,9 @@ def clean_env(monkeypatch): """Remove all env vars that influence auth availability for a clean baseline.""" for var in ( "CONTAINER_NAME", + "CUA_ALLOW_UNAUTHENTICATED_LOCAL", + "CUA_ENABLE_RUN_COMMAND", + "CUA_ENABLE_PUBLIC_PROXY", "UNAVAILABLE_WITHOUT_CONTAINER_NAME", "UNAVAILABLE_WITHOUT_CONTAINER_NAME_RESPONSE_STATUS_CODE", ): @@ -84,8 +91,13 @@ def test_invalid_status_code_falls_back_to_503(self, clean_env): class TestCmdEndpoint: """Integration tests for the POST /cmd endpoint.""" - def test_backwards_compat_local_dev_allows_requests(self, clean_env, client): - # No CONTAINER_NAME, no availability flag — old "local dev" behavior. + def test_default_local_dev_rejects_requests(self, clean_env, client): + resp = client.post("/cmd", json={"command": "version", "params": {}}) + assert resp.status_code == 401, resp.text + assert "Authentication required" in resp.json()["detail"] + + def test_explicit_local_dev_allows_requests(self, clean_env, client): + clean_env.setenv("CUA_ALLOW_UNAUTHENTICATED_LOCAL", "1") resp = client.post("/cmd", json={"command": "version", "params": {}}) assert resp.status_code == 200, resp.text assert "success" in resp.text @@ -110,16 +122,33 @@ def test_container_name_set_bypasses_unavailable_flag(self, clean_env, client): resp = client.post("/cmd", json={"command": "version", "params": {}}) assert resp.status_code == 401 + def test_run_command_disabled_by_default(self, clean_env, client): + clean_env.setenv("CUA_ALLOW_UNAUTHENTICATED_LOCAL", "1") + resp = client.post("/cmd", json={"command": "shell", "params": {"command": "echo hi"}}) + assert resp.status_code == 403 + assert "CUA_ENABLE_RUN_COMMAND" in resp.json()["detail"] + class TestPtyEndpointAuthGate: """Integration tests for PTY endpoints (via `_require_auth`).""" - def test_backwards_compat_local_dev_allows_access(self, clean_env, client): + def test_default_local_dev_rejects_access(self, clean_env, client): + resp = client.get("/pty/999999") + assert resp.status_code == 401 + + def test_explicit_local_dev_allows_access(self, clean_env, client): + clean_env.setenv("CUA_ALLOW_UNAUTHENTICATED_LOCAL", "1") # Use a non-existent PID — we just want to verify we get past the auth gate. # If auth passes, we get 404 (PTY not found); if not, we get 401/503. resp = client.get("/pty/999999") assert resp.status_code == 404, resp.text + def test_pty_creation_disabled_by_default(self, clean_env, client): + clean_env.setenv("CUA_ALLOW_UNAUTHENTICATED_LOCAL", "1") + resp = client.post("/pty", json={"command": "echo hi"}) + assert resp.status_code == 403 + assert "CUA_ENABLE_RUN_COMMAND" in resp.json()["detail"] + def test_unavailable_flag_rejects_with_default_503(self, clean_env, client): clean_env.setenv("UNAVAILABLE_WITHOUT_CONTAINER_NAME", "true") resp = client.get("/pty/999999") @@ -133,7 +162,12 @@ def test_unavailable_flag_with_custom_status_code(self, clean_env, client): class TestPlaywrightExecEndpoint: - def test_backwards_compat_local_dev_accepts_auth(self, clean_env, client): + def test_default_local_dev_rejects_auth(self, clean_env, client): + resp = client.post("/playwright_exec", json={"command": "noop", "params": {}}) + assert resp.status_code == 401 + + def test_explicit_local_dev_accepts_auth(self, clean_env, client): + clean_env.setenv("CUA_ALLOW_UNAUTHENTICATED_LOCAL", "1") # Browser manager may fail for other reasons, but it should NOT be 503/401. resp = client.post("/playwright_exec", json={"command": "noop", "params": {}}) assert resp.status_code not in (401, 503) @@ -144,6 +178,95 @@ def test_unavailable_flag_rejects(self, clean_env, client): assert resp.status_code == 503 +class TestAgentResponsesEndpoint: + def test_default_local_dev_rejects_auth_before_body_validation(self, clean_env, client): + resp = client.post("/responses", json={}) + if not main.HAS_AGENT: + assert resp.status_code == 501 + return + assert resp.status_code == 401 + assert "Authentication required" in resp.json()["detail"] + + def test_explicit_local_dev_reaches_body_validation(self, clean_env, client): + clean_env.setenv("CUA_ALLOW_UNAUTHENTICATED_LOCAL", "1") + resp = client.post("/responses", json={}) + if not main.HAS_AGENT: + assert resp.status_code == 501 + return + assert resp.status_code == 400 + assert "'model' and 'input' are required" in resp.json()["detail"] + + +class TestCommandsEndpoint: + def test_static_command_metadata_available_before_handler_initialization(self, clean_env, client): + resp = client.get("/commands") + assert resp.status_code == 200 + + commands = resp.json()["commands"] + assert commands["launch"]["params"] == [ + {"name": "app", "required": True, "default": None}, + {"name": "args", "required": False, "default": None}, + ] + assert commands["run_command"]["params"] == [ + {"name": "command", "required": True, "default": None}, + {"name": "timeout", "required": False, "default": None}, + ] + assert commands["write_bytes"]["params"] == [ + {"name": "path", "required": True, "default": None}, + {"name": "content_b64", "required": True, "default": None}, + {"name": "append", "required": False, "default": False}, + ] + + def test_fresh_android_commands_metadata_lists_multitouch( + self, clean_env, client, monkeypatch + ): + monkeypatch.setattr(main, "OS_TYPE", "android") + monkeypatch.setattr(main, "_command_handlers", None) + + resp = client.get("/commands") + assert resp.status_code == 200 + assert resp.json()["commands"]["multitouch_gesture"]["params"] == [ + {"name": "fingers", "required": True, "default": None}, + {"name": "screen_w", "required": True, "default": None}, + {"name": "screen_h", "required": True, "default": None}, + {"name": "duration_ms", "required": False, "default": 400}, + {"name": "steps", "required": False, "default": 0}, + ] + + def test_dynamic_android_command_initializes_before_unknown_rejection( + self, clean_env, client, monkeypatch + ): + async def fake_multitouch_gesture(): + return {"success": True} + + monkeypatch.setattr(main, "OS_TYPE", "android") + monkeypatch.setattr(main, "_command_handlers", None) + monkeypatch.setattr( + main, + "_get_command_handlers", + lambda: {"multitouch_gesture": fake_multitouch_gesture}, + ) + clean_env.setenv("CUA_ALLOW_UNAUTHENTICATED_LOCAL", "1") + + resp = client.post("/cmd", json={"command": "multitouch_gesture", "params": {}}) + assert resp.status_code == 200 + assert '"success": true' in resp.text + + def test_non_android_multitouch_rejection_does_not_initialize_handlers( + self, clean_env, client, monkeypatch + ): + def fail_get_handlers(): + raise AssertionError("should not initialize handlers") + + monkeypatch.setattr(main, "OS_TYPE", "darwin") + monkeypatch.setattr(main, "_get_command_handlers", fail_get_handlers) + clean_env.setenv("CUA_ALLOW_UNAUTHENTICATED_LOCAL", "1") + + resp = client.post("/cmd", json={"command": "multitouch_gesture", "params": {}}) + assert resp.status_code == 400 + assert "Unknown command" in resp.json()["detail"] + + class TestStatusEndpointMiddlewareGating: """The middleware applies uniformly — /status is reachable when the flag is off.""" @@ -165,12 +288,28 @@ def test_status_accessible_when_container_name_set(self, clean_env, client): class TestWebSocketEndpoint: - def test_backwards_compat_local_dev_allows_commands(self, clean_env, client): + def test_default_local_dev_rejects_commands(self, clean_env, client): + with client.websocket_connect("/ws") as ws: + data = ws.receive_json() + assert data["success"] is False + assert data["status_code"] == 401 + assert "Authentication required" in data["error"] + + def test_explicit_local_dev_allows_commands(self, clean_env, client): + clean_env.setenv("CUA_ALLOW_UNAUTHENTICATED_LOCAL", "1") with client.websocket_connect("/ws") as ws: ws.send_json({"command": "version", "params": {}}) data = ws.receive_json() assert data["success"] is True + def test_run_command_disabled_by_default(self, clean_env, client): + clean_env.setenv("CUA_ALLOW_UNAUTHENTICATED_LOCAL", "1") + with client.websocket_connect("/ws") as ws: + ws.send_json({"command": "shell", "params": {"command": "echo hi"}}) + data = ws.receive_json() + assert data["success"] is False + assert "CUA_ENABLE_RUN_COMMAND" in data["error"] + def test_unavailable_flag_closes_with_error(self, clean_env, client): clean_env.setenv("UNAVAILABLE_WITHOUT_CONTAINER_NAME", "true") with client.websocket_connect("/ws") as ws: diff --git a/libs/python/computer-server/tests/test_import_safety.py b/libs/python/computer-server/tests/test_import_safety.py new file mode 100644 index 0000000000..8cecb6c6d4 --- /dev/null +++ b/libs/python/computer-server/tests/test_import_safety.py @@ -0,0 +1,37 @@ +import platform +import subprocess +import sys + +import pytest + + +def _assert_import_exits_cleanly(import_statement: str): + result = subprocess.run( + [sys.executable, "-X", "faulthandler", "-c", f"{import_statement}; print('ok')"], + capture_output=True, + text=True, + timeout=20, + ) + + assert result.returncode == 0, result.stdout + result.stderr + assert "ok" in result.stdout + + +def test_package_import_does_not_import_native_handlers_at_teardown(): + _assert_import_exits_cleanly("import computer_server") + + +def test_generic_handler_import_does_not_import_native_handlers_at_teardown(): + _assert_import_exits_cleanly("import computer_server.handlers.generic") + + +def test_main_import_does_not_create_native_handlers_at_teardown(): + _assert_import_exits_cleanly("import computer_server.main") + + +@pytest.mark.skipif(platform.system() != "Darwin", reason="macOS handler requires AppKit") +def test_macos_handler_creation_exits_cleanly_at_teardown(): + _assert_import_exits_cleanly( + "from computer_server.handlers.factory import HandlerFactory; " + "HandlerFactory.create_handlers()" + ) diff --git a/libs/python/computer-server/tests/test_window_launch.py b/libs/python/computer-server/tests/test_window_launch.py new file mode 100644 index 0000000000..5462a51a79 --- /dev/null +++ b/libs/python/computer-server/tests/test_window_launch.py @@ -0,0 +1,43 @@ +from unittest.mock import Mock, patch + +import pytest + +from computer_server.handlers.generic import GenericWindowHandler, build_launch_argv + + +def test_build_launch_argv_splits_compatible_command_string(): + assert build_launch_argv("libreoffice --writer") == ["libreoffice", "--writer"] + + +def test_build_launch_argv_keeps_shell_metacharacters_literal(): + assert build_launch_argv("echo hello; touch /tmp/pwned") == [ + "echo", + "hello;", + "touch", + "/tmp/pwned", + ] + + +def test_build_launch_argv_uses_explicit_args_literally(): + assert build_launch_argv("echo", ["hello; touch /tmp/pwned"]) == [ + "echo", + "hello; touch /tmp/pwned", + ] + + +@pytest.mark.asyncio +async def test_launch_uses_argv_without_shell(): + proc = Mock(pid=1234) + with patch("computer_server.handlers.generic.subprocess.Popen", return_value=proc) as popen: + result = await GenericWindowHandler().launch("echo hello; touch /tmp/pwned") + + assert result == {"success": True, "pid": 1234} + popen.assert_called_once_with(["echo", "hello;", "touch", "/tmp/pwned"]) + + +@pytest.mark.asyncio +async def test_launch_rejects_empty_app(): + result = await GenericWindowHandler().launch("") + + assert result["success"] is False + assert "app must not be empty" in result["error"] diff --git a/libs/python/cua-auto/cua_auto/window.py b/libs/python/cua-auto/cua_auto/window.py index df0aa5bbea..9eaa50d746 100644 --- a/libs/python/cua-auto/cua_auto/window.py +++ b/libs/python/cua-auto/cua_auto/window.py @@ -2,6 +2,7 @@ import os import platform +import shlex import subprocess import webbrowser from typing import Any, List, Optional, Tuple @@ -30,6 +31,15 @@ def _get_by_handle(handle: Any) -> Optional[Any]: return None +def _build_launch_argv(app: str, args: Optional[List[str]] = None) -> List[str]: + if args is not None: + return [app, *args] + argv = shlex.split(app) + if not argv: + raise ValueError("app must not be empty") + return argv + + # ── Active window ───────────────────────────────────────────────────────────── @@ -177,12 +187,8 @@ def open(target: str) -> bool: # noqa: A001 def launch(app: str, args: Optional[List[str]] = None) -> int: """Launch an application and return its PID. - If *args* is given, the app is launched with those arguments. - Otherwise the command string is passed to the system shell, allowing - strings like ``"libreoffice --writer"``. + If *args* is given, the app is launched with those literal arguments. + Otherwise a simple command string is split into argv form. """ - if args: - proc = subprocess.Popen([app, *args]) - else: - proc = subprocess.Popen(app, shell=True) + proc = subprocess.Popen(_build_launch_argv(app, args)) return proc.pid diff --git a/libs/python/cua-auto/tests/test_window_launch.py b/libs/python/cua-auto/tests/test_window_launch.py new file mode 100644 index 0000000000..e467c86c3b --- /dev/null +++ b/libs/python/cua-auto/tests/test_window_launch.py @@ -0,0 +1,38 @@ +from unittest.mock import Mock, patch + +import pytest + +from cua_auto.window import _build_launch_argv, launch + + +def test_build_launch_argv_splits_compatible_command_string(): + assert _build_launch_argv("libreoffice --writer") == ["libreoffice", "--writer"] + + +def test_build_launch_argv_keeps_shell_metacharacters_literal(): + assert _build_launch_argv("echo hello; touch /tmp/pwned") == [ + "echo", + "hello;", + "touch", + "/tmp/pwned", + ] + + +def test_build_launch_argv_uses_explicit_args_literally(): + assert _build_launch_argv("echo", ["hello; touch /tmp/pwned"]) == [ + "echo", + "hello; touch /tmp/pwned", + ] + + +def test_launch_uses_argv_without_shell(): + proc = Mock(pid=1234) + with patch("cua_auto.window.subprocess.Popen", return_value=proc) as popen: + assert launch("echo hello; touch /tmp/pwned") == 1234 + + popen.assert_called_once_with(["echo", "hello;", "touch", "/tmp/pwned"]) + + +def test_launch_rejects_empty_app(): + with pytest.raises(ValueError, match="app must not be empty"): + launch("") diff --git a/libs/python/cua-cli/pyproject.toml b/libs/python/cua-cli/pyproject.toml index 081f41b5ba..1a2e3ab6f1 100644 --- a/libs/python/cua-cli/pyproject.toml +++ b/libs/python/cua-cli/pyproject.toml @@ -45,11 +45,11 @@ dependencies = [ [project.optional-dependencies] # MCP server support mcp = [ - "fastmcp>=2.0", + "fastmcp>=3.2.0", ] # Skills recording with LLM captioning skills = [ - "litellm==1.80.0", + "litellm>=1.83.7,<1.84", ] # Full installation all = [ diff --git a/libs/python/cua-cli/uv.lock b/libs/python/cua-cli/uv.lock index 7996b5c95a..da8755eb08 100644 --- a/libs/python/cua-cli/uv.lock +++ b/libs/python/cua-cli/uv.lock @@ -25,7 +25,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.13.3" +version = "3.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -36,42 +36,42 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, - { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, - { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, - { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, - { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, - { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, - { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, - { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, - { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, - { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, - { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, - { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, - { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, - { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, - { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, - { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, - { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, - { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, - { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, - { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, - { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, - { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, - { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, - { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, - { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/45/4a/064321452809dae953c1ed6e017504e72551a26b6f5708a5a80e4bf556ff/aiohttp-3.13.4.tar.gz", hash = "sha256:d97a6d09c66087890c2ab5d49069e1e570583f7ac0314ecf98294c1b6aaebd38", size = 7859748, upload-time = "2026-03-28T17:19:40.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/bd/ede278648914cabbabfdf95e436679b5d4156e417896a9b9f4587169e376/aiohttp-3.13.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ee62d4471ce86b108b19c3364db4b91180d13fe3510144872d6bad5401957360", size = 752158, upload-time = "2026-03-28T17:16:06.901Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/581c053253c07b480b03785196ca5335e3c606a37dc73e95f6527f1591fe/aiohttp-3.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c0fd8f41b54b58636402eb493afd512c23580456f022c1ba2db0f810c959ed0d", size = 501037, upload-time = "2026-03-28T17:16:08.82Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/a5ede193c08f13cc42c0a5b50d1e246ecee9115e4cf6e900d8dbd8fd6acb/aiohttp-3.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4baa48ce49efd82d6b1a0be12d6a36b35e5594d1dd42f8bfba96ea9f8678b88c", size = 501556, upload-time = "2026-03-28T17:16:10.63Z" }, + { url = "https://files.pythonhosted.org/packages/d6/10/88ff67cd48a6ec36335b63a640abe86135791544863e0cfe1f065d6cef7a/aiohttp-3.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d738ebab9f71ee652d9dbd0211057690022201b11197f9a7324fd4dba128aa97", size = 1757314, upload-time = "2026-03-28T17:16:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/8b/15/fdb90a5cf5a1f52845c276e76298c75fbbcc0ac2b4a86551906d54529965/aiohttp-3.13.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0ce692c3468fa831af7dceed52edf51ac348cebfc8d3feb935927b63bd3e8576", size = 1731819, upload-time = "2026-03-28T17:16:14.558Z" }, + { url = "https://files.pythonhosted.org/packages/ec/df/28146785a007f7820416be05d4f28cc207493efd1e8c6c1068e9bdc29198/aiohttp-3.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e08abcfe752a454d2cb89ff0c08f2d1ecd057ae3e8cc6d84638de853530ebab", size = 1793279, upload-time = "2026-03-28T17:16:16.594Z" }, + { url = "https://files.pythonhosted.org/packages/10/47/689c743abf62ea7a77774d5722f220e2c912a77d65d368b884d9779ef41b/aiohttp-3.13.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5977f701b3fff36367a11087f30ea73c212e686d41cd363c50c022d48b011d8d", size = 1891082, upload-time = "2026-03-28T17:16:18.71Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/f7f4f318c7e58c23b761c9b13b9a3c9b394e0f9d5d76fbc6622fa98509f6/aiohttp-3.13.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54203e10405c06f8b6020bd1e076ae0fe6c194adcee12a5a78af3ffa3c57025e", size = 1773938, upload-time = "2026-03-28T17:16:21.125Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/f207cb3121852c989586a6fc16ff854c4fcc8651b86c5d3bd1fc83057650/aiohttp-3.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:358a6af0145bc4dda037f13167bef3cce54b132087acc4c295c739d05d16b1c3", size = 1579548, upload-time = "2026-03-28T17:16:23.588Z" }, + { url = "https://files.pythonhosted.org/packages/6c/58/e1289661a32161e24c1fe479711d783067210d266842523752869cc1d9c2/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:898ea1850656d7d61832ef06aa9846ab3ddb1621b74f46de78fbc5e1a586ba83", size = 1714669, upload-time = "2026-03-28T17:16:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/96/0a/3e86d039438a74a86e6a948a9119b22540bae037d6ba317a042ae3c22711/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7bc30cceb710cf6a44e9617e43eebb6e3e43ad855a34da7b4b6a73537d8a6763", size = 1754175, upload-time = "2026-03-28T17:16:28.18Z" }, + { url = "https://files.pythonhosted.org/packages/f4/30/e717fc5df83133ba467a560b6d8ef20197037b4bb5d7075b90037de1018e/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4a31c0c587a8a038f19a4c7e60654a6c899c9de9174593a13e7cc6e15ff271f9", size = 1762049, upload-time = "2026-03-28T17:16:30.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/28/8f7a2d4492e336e40005151bdd94baf344880a4707573378579f833a64c1/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2062f675f3fe6e06d6113eb74a157fb9df58953ffed0cdb4182554b116545758", size = 1570861, upload-time = "2026-03-28T17:16:32.953Z" }, + { url = "https://files.pythonhosted.org/packages/78/45/12e1a3d0645968b1c38de4b23fdf270b8637735ea057d4f84482ff918ad9/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d1ba8afb847ff80626d5e408c1fdc99f942acc877d0702fe137015903a220a9", size = 1790003, upload-time = "2026-03-28T17:16:35.468Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/60374e18d590de16dcb39d6ff62f39c096c1b958e6f37727b5870026ea30/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b08149419994cdd4d5eecf7fd4bc5986b5a9380285bcd01ab4c0d6bfca47b79d", size = 1737289, upload-time = "2026-03-28T17:16:38.187Z" }, + { url = "https://files.pythonhosted.org/packages/02/bf/535e58d886cfbc40a8b0013c974afad24ef7632d645bca0b678b70033a60/aiohttp-3.13.4-cp312-cp312-win32.whl", hash = "sha256:fc432f6a2c4f720180959bc19aa37259651c1a4ed8af8afc84dd41c60f15f791", size = 434185, upload-time = "2026-03-28T17:16:40.735Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1a/d92e3325134ebfff6f4069f270d3aac770d63320bd1fcd0eca023e74d9a8/aiohttp-3.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:6148c9ae97a3e8bff9a1fc9c757fa164116f86c100468339730e717590a3fb77", size = 461285, upload-time = "2026-03-28T17:16:42.713Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ac/892f4162df9b115b4758d615f32ec63d00f3084c705ff5526630887b9b42/aiohttp-3.13.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:63dd5e5b1e43b8fb1e91b79b7ceba1feba588b317d1edff385084fcc7a0a4538", size = 745744, upload-time = "2026-03-28T17:16:44.67Z" }, + { url = "https://files.pythonhosted.org/packages/97/a9/c5b87e4443a2f0ea88cb3000c93a8fdad1ee63bffc9ded8d8c8e0d66efc6/aiohttp-3.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:746ac3cc00b5baea424dacddea3ec2c2702f9590de27d837aa67004db1eebc6e", size = 498178, upload-time = "2026-03-28T17:16:46.766Z" }, + { url = "https://files.pythonhosted.org/packages/94/42/07e1b543a61250783650df13da8ddcdc0d0a5538b2bd15cef6e042aefc61/aiohttp-3.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bda8f16ea99d6a6705e5946732e48487a448be874e54a4f73d514660ff7c05d3", size = 498331, upload-time = "2026-03-28T17:16:48.9Z" }, + { url = "https://files.pythonhosted.org/packages/20/d6/492f46bf0328534124772d0cf58570acae5b286ea25006900650f69dae0e/aiohttp-3.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b061e7b5f840391e3f64d0ddf672973e45c4cfff7a0feea425ea24e51530fc2", size = 1744414, upload-time = "2026-03-28T17:16:50.968Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/e02627b2683f68051246215d2d62b2d2f249ff7a285e7a858dc47d6b6a14/aiohttp-3.13.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b252e8d5cd66184b570d0d010de742736e8a4fab22c58299772b0c5a466d4b21", size = 1719226, upload-time = "2026-03-28T17:16:53.173Z" }, + { url = "https://files.pythonhosted.org/packages/7b/6c/5d0a3394dd2b9f9aeba6e1b6065d0439e4b75d41f1fb09a3ec010b43552b/aiohttp-3.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20af8aad61d1803ff11152a26146d8d81c266aa8c5aa9b4504432abb965c36a0", size = 1782110, upload-time = "2026-03-28T17:16:55.362Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2d/c20791e3437700a7441a7edfb59731150322424f5aadf635602d1d326101/aiohttp-3.13.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:13a5cc924b59859ad2adb1478e31f410a7ed46e92a2a619d6d1dd1a63c1a855e", size = 1884809, upload-time = "2026-03-28T17:16:57.734Z" }, + { url = "https://files.pythonhosted.org/packages/c8/94/d99dbfbd1924a87ef643833932eb2a3d9e5eee87656efea7d78058539eff/aiohttp-3.13.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:534913dfb0a644d537aebb4123e7d466d94e3be5549205e6a31f72368980a81a", size = 1764938, upload-time = "2026-03-28T17:17:00.221Z" }, + { url = "https://files.pythonhosted.org/packages/49/61/3ce326a1538781deb89f6cf5e094e2029cd308ed1e21b2ba2278b08426f6/aiohttp-3.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:320e40192a2dcc1cf4b5576936e9652981ab596bf81eb309535db7e2f5b5672f", size = 1570697, upload-time = "2026-03-28T17:17:02.985Z" }, + { url = "https://files.pythonhosted.org/packages/b6/77/4ab5a546857bb3028fbaf34d6eea180267bdab022ee8b1168b1fcde4bfdd/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9e587fcfce2bcf06526a43cb705bdee21ac089096f2e271d75de9c339db3100c", size = 1702258, upload-time = "2026-03-28T17:17:05.28Z" }, + { url = "https://files.pythonhosted.org/packages/79/63/d8f29021e39bc5af8e5d5e9da1b07976fb9846487a784e11e4f4eeda4666/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9eb9c2eea7278206b5c6c1441fdd9dc420c278ead3f3b2cc87f9b693698cc500", size = 1740287, upload-time = "2026-03-28T17:17:07.712Z" }, + { url = "https://files.pythonhosted.org/packages/55/3a/cbc6b3b124859a11bc8055d3682c26999b393531ef926754a3445b99dfef/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:29be00c51972b04bf9d5c8f2d7f7314f48f96070ca40a873a53056e652e805f7", size = 1753011, upload-time = "2026-03-28T17:17:10.053Z" }, + { url = "https://files.pythonhosted.org/packages/e0/30/836278675205d58c1368b21520eab9572457cf19afd23759216c04483048/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90c06228a6c3a7c9f776fe4fc0b7ff647fffd3bed93779a6913c804ae00c1073", size = 1566359, upload-time = "2026-03-28T17:17:12.433Z" }, + { url = "https://files.pythonhosted.org/packages/50/b4/8032cc9b82d17e4277704ba30509eaccb39329dc18d6a35f05e424439e32/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a533ec132f05fd9a1d959e7f34184cd7d5e8511584848dab85faefbaac573069", size = 1785537, upload-time = "2026-03-28T17:17:14.721Z" }, + { url = "https://files.pythonhosted.org/packages/17/7d/5873e98230bde59f493bf1f7c3e327486a4b5653fa401144704df5d00211/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1c946f10f413836f82ea4cfb90200d2a59578c549f00857e03111cf45ad01ca5", size = 1740752, upload-time = "2026-03-28T17:17:17.387Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f2/13e46e0df051494d7d3c68b7f72d071f48c384c12716fc294f75d5b1a064/aiohttp-3.13.4-cp313-cp313-win32.whl", hash = "sha256:48708e2706106da6967eff5908c78ca3943f005ed6bcb75da2a7e4da94ef8c70", size = 433187, upload-time = "2026-03-28T17:17:19.523Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c0/649856ee655a843c8f8664592cfccb73ac80ede6a8c8db33a25d810c12db/aiohttp-3.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:74a2eb058da44fa3a877a49e2095b591d4913308bb424c418b77beb160c55ce3", size = 459778, upload-time = "2026-03-28T17:17:21.964Z" }, ] [[package]] @@ -270,14 +270,14 @@ wheels = [ [[package]] name = "click" -version = "8.3.1" +version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, ] [[package]] @@ -406,7 +406,7 @@ all = [ [[package]] name = "cua-cli" -version = "0.1.8" +version = "0.1.11" source = { editable = "." } dependencies = [ { name = "aiohttp" }, @@ -443,9 +443,9 @@ requires-dist = [ { name = "cua-auto", extras = ["all"], specifier = ">=0.1.0" }, { name = "cua-cli", extras = ["mcp", "skills"], marker = "extra == 'all'" }, { name = "cua-computer", specifier = ">=0.5.0" }, - { name = "cua-core", specifier = ">=0.1.18" }, - { name = "fastmcp", marker = "extra == 'mcp'", specifier = ">=2.0" }, - { name = "litellm", marker = "extra == 'skills'", specifier = ">=1.74.0" }, + { name = "cua-core", specifier = ">=0.3.0,<0.4.0" }, + { name = "fastmcp", marker = "extra == 'mcp'", specifier = ">=3.2.0" }, + { name = "litellm", marker = "extra == 'skills'", specifier = ">=1.83.7,<1.84" }, { name = "pillow", specifier = ">=10.0.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.0" }, @@ -459,7 +459,7 @@ provides-extras = ["mcp", "skills", "all", "dev"] [[package]] name = "cua-computer" -version = "0.5.17" +version = "0.5.18" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -470,21 +470,21 @@ dependencies = [ { name = "websocket-client" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f9/42/932b8008cd15e40651b3bef4df700feeca7f962f9220fca69c27b800b907/cua_computer-0.5.17.tar.gz", hash = "sha256:8a8881adbbdbf5e65ae7b1cf93e3f1d9c2781fbadd90acd5de757771b5e312f7", size = 101747, upload-time = "2026-03-04T15:47:38.565Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/b1/0ad69b0fd25bb11b2f68c7db5481b8a020763a354f1f636698fee00f383d/cua_computer-0.5.18.tar.gz", hash = "sha256:4f15df2dffc60af7cf80e0954186b12301ee08fd12d6fdf8b531ec4c63650837", size = 102054, upload-time = "2026-04-15T21:22:03.49Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/ba/cd691109cba3251302c1b5bdbddfc69a85bb04ccb37984737c4389a12f55/cua_computer-0.5.17-py3-none-any.whl", hash = "sha256:1c4fb866ea8a0ec2c05b853306437fc38bdd7e2acd4772e4811303a80f08697b", size = 117364, upload-time = "2026-03-04T15:47:37.544Z" }, + { url = "https://files.pythonhosted.org/packages/ed/52/1d0ba187276259bd1306a4a568cf50fedc9b0ac48743a60629d6ba21ed81/cua_computer-0.5.18-py3-none-any.whl", hash = "sha256:afeff75460cacf4c2a112f9aeb972da3dfe134db9e916c71b8ae1466e67e0e85", size = 117684, upload-time = "2026-04-15T21:22:02.088Z" }, ] [[package]] name = "cua-core" -version = "0.1.18" +version = "0.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "posthog" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/62/07/92955e3643fc41f090b41cd7dc699c7dd4a50c873557e060bb2fa19ccc1b/cua_core-0.1.18.tar.gz", hash = "sha256:8c05c96b3942214769d1ef79d09265b416de4a7c7d647d21541878c744221858", size = 10499, upload-time = "2026-02-25T20:43:02.752Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/82/ec371bde395bbbd326a3467a1702b34ab629defa0cc89bd81ac78507ca5a/cua_core-0.3.1.tar.gz", hash = "sha256:a8fc4204981b9d5b604bd2a34de0bb41cf926309e4dd9f364c827750a86408da", size = 10710, upload-time = "2026-04-15T21:39:49.172Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/83/719c32bed7741af5759812ea214c3ea5bb7b7878cac86cb390468b2856fc/cua_core-0.1.18-py3-none-any.whl", hash = "sha256:c66ea5b2b8f0a0d1902efe2cf05e068fa34f4fc4acde1e8f9b6be37a314a6237", size = 9825, upload-time = "2026-02-25T20:43:02.013Z" }, + { url = "https://files.pythonhosted.org/packages/ed/90/5148998a26671855539e2c3a620f72ef51f83118beffdfb7e77893d4730b/cua_core-0.3.1-py3-none-any.whl", hash = "sha256:9191b8abf5e02ea6bf8ba21237e747d4e3b319f6f24d4e66e04b7606fcc449f6", size = 10239, upload-time = "2026-04-15T21:39:48.265Z" }, ] [[package]] @@ -583,12 +583,13 @@ wheels = [ [[package]] name = "fastmcp" -version = "3.1.0" +version = "3.2.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "authlib" }, { name = "cyclopts" }, { name = "exceptiongroup" }, + { name = "griffelib" }, { name = "httpx" }, { name = "jsonref" }, { name = "jsonschema-path" }, @@ -608,9 +609,9 @@ dependencies = [ { name = "watchfiles" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0a/70/862026c4589441f86ad3108f05bfb2f781c6b322ad60a982f40b303b47d7/fastmcp-3.1.0.tar.gz", hash = "sha256:e25264794c734b9977502a51466961eeecff92a0c2f3b49c40c070993628d6d0", size = 17347083, upload-time = "2026-03-03T02:43:11.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/13/29544fbc6dfe45ea38046af0067311e0bad7acc7d1f2ad38bb08f2409fe2/fastmcp-3.2.4.tar.gz", hash = "sha256:083ecb75b44a4169e7fc0f632f94b781bdb0ff877c6b35b9877cbb566fd4d4d1", size = 28746127, upload-time = "2026-04-14T01:42:24.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/07/516f5b20d88932e5a466c2216b628e5358a71b3a9f522215607c3281de05/fastmcp-3.1.0-py3-none-any.whl", hash = "sha256:b1f73b56fd3b0cb2bd9e2a144fc650d5cc31587ed129d996db7710e464ae8010", size = 633749, upload-time = "2026-03-03T02:43:09.06Z" }, + { url = "https://files.pythonhosted.org/packages/cf/76/b310d52fa0e30d39bd937eb58ec2c1f1ea1b5f519f0575e9dd9612f01deb/fastmcp-3.2.4-py3-none-any.whl", hash = "sha256:e6c9c429171041455e47ab94bb3f83c4657622a0ec28922f6940053959bd58a9", size = 728599, upload-time = "2026-04-14T01:42:26.85Z" }, ] [[package]] @@ -718,6 +719,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, ] +[[package]] +name = "griffelib" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -819,14 +829,14 @@ wheels = [ [[package]] name = "importlib-metadata" -version = "8.7.1" +version = "8.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload-time = "2024-09-11T14:56:08.937Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload-time = "2024-09-11T14:56:07.019Z" }, ] [[package]] @@ -946,7 +956,7 @@ wheels = [ [[package]] name = "jsonschema" -version = "4.26.0" +version = "4.23.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -954,9 +964,9 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778, upload-time = "2024-07-08T18:40:05.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, + { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462, upload-time = "2024-07-08T18:40:00.165Z" }, ] [[package]] @@ -1004,7 +1014,7 @@ wheels = [ [[package]] name = "litellm" -version = "1.82.1" +version = "1.83.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -1020,9 +1030,9 @@ dependencies = [ { name = "tiktoken" }, { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/bd/6251e9a965ae2d7bc3342ae6c1a2d25dd265d354c502e63225451b135016/litellm-1.82.1.tar.gz", hash = "sha256:bc8427cdccc99e191e08e36fcd631c93b27328d1af789839eb3ac01a7d281890", size = 17197496, upload-time = "2026-03-10T09:10:04.438Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/7c/c095649380adc96c8630273c1768c2ad1e74aa2ee1dd8dd05d218a60569f/litellm-1.83.14.tar.gz", hash = "sha256:24aef9b47cdc424c833e32f3727f411741c690832cd1fe4405e0077144fe09c9", size = 14836599, upload-time = "2026-04-26T03:16:10.176Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/77/0c6eca2cb049793ddf8ce9cdcd5123a35666c4962514788c4fc90edf1d3b/litellm-1.82.1-py3-none-any.whl", hash = "sha256:a9ec3fe42eccb1611883caaf8b1bf33c9f4e12163f94c7d1004095b14c379eb2", size = 15341896, upload-time = "2026-03-10T09:10:00.702Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5c/1b5691575420135e90578543b2bf219497caa33cfd0af64cb38f30288450/litellm-1.83.14-py3-none-any.whl", hash = "sha256:92b11ba2a32cf80707ddf388d18526696c7999a21b418c5e3b6eda1243d2cfdb", size = 16457054, upload-time = "2026-04-26T03:16:05.72Z" }, ] [[package]] @@ -1204,7 +1214,7 @@ wheels = [ [[package]] name = "openai" -version = "2.28.0" +version = "2.24.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1216,9 +1226,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/56/87/eb0abb4ef88ddb95b3c13149384c4c288f584f3be17d6a4f63f8c3e3c226/openai-2.28.0.tar.gz", hash = "sha256:bb7fdff384d2a787fa82e8822d1dd3c02e8cf901d60f1df523b7da03cbb6d48d", size = 670334, upload-time = "2026-03-13T19:56:27.306Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/13/17e87641b89b74552ed408a92b231283786523edddc95f3545809fab673c/openai-2.24.0.tar.gz", hash = "sha256:1e5769f540dbd01cb33bc4716a23e67b9d695161a734aff9c5f925e2bf99a673", size = 658717, upload-time = "2026-02-24T20:02:07.958Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/5a/df122348638885526e53140e9c6b0d844af7312682b3bde9587eebc28b47/openai-2.28.0-py3-none-any.whl", hash = "sha256:79aa5c45dba7fef84085701c235cf13ba88485e1ef4f8dfcedc44fc2a698fc1d", size = 1141218, upload-time = "2026-03-13T19:56:25.46Z" }, + { url = "https://files.pythonhosted.org/packages/c9/30/844dc675ee6902579b8eef01ed23917cc9319a1c9c0c14ec6e39340c96d0/openai-2.24.0-py3-none-any.whl", hash = "sha256:fed30480d7d6c884303287bde864980a4b137b60553ffbcf9ab4a233b7a73d94", size = 1120122, upload-time = "2026-02-24T20:02:05.669Z" }, ] [[package]] @@ -4593,7 +4603,7 @@ wheels = [ [[package]] name = "typer" -version = "0.24.1" +version = "0.23.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -4601,9 +4611,9 @@ dependencies = [ { name = "rich" }, { name = "shellingham" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/07/b822e1b307d40e263e8253d2384cf98c51aa2368cc7ba9a07e523a1d964b/typer-0.23.1.tar.gz", hash = "sha256:2070374e4d31c83e7b61362fd859aa683576432fd5b026b060ad6b4cd3b86134", size = 120047, upload-time = "2026-02-13T10:04:30.984Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/9b286ab899c008c2cb05e8be99814807e7fbbd33f0c0c960470826e5ac82/typer-0.23.1-py3-none-any.whl", hash = "sha256:3291ad0d3c701cbf522012faccfbb29352ff16ad262db2139e6b01f15781f14e", size = 56813, upload-time = "2026-02-13T10:04:32.008Z" }, ] [[package]] diff --git a/libs/python/cua-sandbox/uv.lock b/libs/python/cua-sandbox/uv.lock index 8358b604e8..108f0bc1d0 100644 --- a/libs/python/cua-sandbox/uv.lock +++ b/libs/python/cua-sandbox/uv.lock @@ -332,14 +332,14 @@ wheels = [ [[package]] name = "click" -version = "8.3.1" +version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, ] [[package]] @@ -407,7 +407,7 @@ wheels = [ [[package]] name = "cua-agent" -version = "0.7.39" +version = "0.8.2" source = { editable = "../agent" } dependencies = [ { name = "aiohttp" }, @@ -450,7 +450,7 @@ requires-dist = [ { name = "gradio", marker = "extra == 'ui'", specifier = ">=6.0.0" }, { name = "httpx", specifier = ">=0.27.0" }, { name = "hud-python", marker = "extra == 'hud'", specifier = "==0.4.52" }, - { name = "litellm", specifier = "==1.80.0" }, + { name = "litellm", specifier = ">=1.83.7,<1.84" }, { name = "mlx-vlm", marker = "sys_platform == 'darwin' and extra == 'all'", specifier = ">=0.1.27" }, { name = "mlx-vlm", marker = "sys_platform == 'darwin' and extra == 'uitars-mlx'", specifier = ">=0.1.27" }, { name = "pillow", specifier = ">=10.0.0" }, @@ -479,11 +479,11 @@ requires-dist = [ { name = "torch", marker = "extra == 'opencua-hf'" }, { name = "torch", marker = "extra == 'uitars-hf'" }, { name = "transformers", marker = "extra == 'all'", specifier = ">=4.55.0" }, + { name = "transformers", marker = "extra == 'glm45v-hf'", specifier = ">=4.57.6,<5" }, { name = "transformers", marker = "extra == 'internvl-hf'", specifier = ">=4.55.0" }, { name = "transformers", marker = "extra == 'moondream3'", specifier = ">=4.55.0" }, { name = "transformers", marker = "extra == 'opencua-hf'", specifier = ">=4.53.0" }, { name = "transformers", marker = "extra == 'uitars-hf'", specifier = ">=4.54.0" }, - { name = "transformers-v4-55-0-glm-4-5v-preview", marker = "extra == 'glm45v-hf'" }, { name = "typing-extensions", specifier = ">=4.12.2" }, { name = "yaspin", marker = "extra == 'all'", specifier = ">=3.1.0" }, { name = "yaspin", marker = "extra == 'cli'", specifier = ">=3.1.0" }, @@ -508,7 +508,7 @@ wheels = [ [[package]] name = "cua-core" -version = "0.1.19" +version = "0.3.1" source = { editable = "../core" } dependencies = [ { name = "posthog" }, @@ -531,7 +531,7 @@ dev = [{ name = "pytest", specifier = ">=8.3.5" }] [[package]] name = "cua-sandbox" -version = "0.1.3" +version = "0.1.16" source = { editable = "." } dependencies = [ { name = "cua-auto" }, @@ -566,7 +566,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "cua-auto", specifier = ">=0.1.2" }, - { name = "cua-core", specifier = ">=0.1.18,<0.2.0" }, + { name = "cua-core", specifier = ">=0.3.0,<0.4.0" }, { name = "grpcio", specifier = "==1.78.0" }, { name = "httpx", specifier = ">=0.27.0" }, { name = "oras", specifier = ">=0.2.40" }, @@ -893,14 +893,14 @@ wheels = [ [[package]] name = "importlib-metadata" -version = "9.0.0" +version = "8.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload-time = "2024-09-11T14:56:08.937Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload-time = "2024-09-11T14:56:07.019Z" }, ] [[package]] @@ -1007,7 +1007,7 @@ wheels = [ [[package]] name = "jsonschema" -version = "4.26.0" +version = "4.23.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -1015,9 +1015,9 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778, upload-time = "2024-07-08T18:40:05.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, + { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462, upload-time = "2024-07-08T18:40:00.165Z" }, ] [[package]] @@ -1034,7 +1034,7 @@ wheels = [ [[package]] name = "litellm" -version = "1.80.0" +version = "1.83.13" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -1050,9 +1050,9 @@ dependencies = [ { name = "tiktoken" }, { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bd/8c/48d533affdbc6d485b7ad4221cd3b40b8c12f9f5568edfe0be0b11e7b945/litellm-1.80.0.tar.gz", hash = "sha256:eeac733eb6b226f9e5fb020f72fe13a32b3354b001dc62bcf1bc4d9b526d6231", size = 11591976, upload-time = "2025-11-16T00:03:51.812Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/ac/6cc660c476e1cb925055eb7d032b7774de71fce28b9dfec652fc277397f8/litellm-1.83.13.tar.gz", hash = "sha256:803b53077cd678bb78d40f1a971920a8cd06073520c756e3d2bcb5c4da7bd352", size = 14785817, upload-time = "2026-04-24T01:03:03.096Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/53/aa31e4d057b3746b3c323ca993003d6cf15ef987e7fe7ceb53681695ae87/litellm-1.80.0-py3-none-any.whl", hash = "sha256:fd0009758f4772257048d74bf79bb64318859adb4ea49a8b66fdbc718cd80b6e", size = 10492975, upload-time = "2025-11-16T00:03:49.182Z" }, + { url = "https://files.pythonhosted.org/packages/c5/57/99d553839018ba4c1b94fbe540f218fefb725fba79ecc2abf462cd39d719/litellm-1.83.13-py3-none-any.whl", hash = "sha256:14be9f3b4a9e207372f275d75e6f174a3e717352ccca6c63707c25fd6e23cac6", size = 16406883, upload-time = "2026-04-24T01:02:58.7Z" }, ] [[package]] @@ -1211,7 +1211,7 @@ wheels = [ [[package]] name = "openai" -version = "2.30.0" +version = "2.24.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1223,9 +1223,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/88/15/52580c8fbc16d0675d516e8749806eda679b16de1e4434ea06fb6feaa610/openai-2.30.0.tar.gz", hash = "sha256:92f7661c990bda4b22a941806c83eabe4896c3094465030dd882a71abe80c885", size = 676084, upload-time = "2026-03-25T22:08:59.96Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/13/17e87641b89b74552ed408a92b231283786523edddc95f3545809fab673c/openai-2.24.0.tar.gz", hash = "sha256:1e5769f540dbd01cb33bc4716a23e67b9d695161a734aff9c5f925e2bf99a673", size = 658717, upload-time = "2026-02-24T20:02:07.958Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/9e/5bfa2270f902d5b92ab7d41ce0475b8630572e71e349b2a4996d14bdda93/openai-2.30.0-py3-none-any.whl", hash = "sha256:9a5ae616888eb2748ec5e0c5b955a51592e0b201a11f4262db920f2a78c5231d", size = 1146656, upload-time = "2026-03-25T22:08:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/c9/30/844dc675ee6902579b8eef01ed23917cc9319a1c9c0c14ec6e39340c96d0/openai-2.24.0-py3-none-any.whl", hash = "sha256:fed30480d7d6c884303287bde864980a4b137b60553ffbcf9ab4a233b7a73d94", size = 1120122, upload-time = "2026-02-24T20:02:05.669Z" }, ] [[package]] @@ -4280,11 +4280,11 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.2.2" +version = "1.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115, upload-time = "2024-01-23T06:33:00.505Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, + { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863, upload-time = "2024-01-23T06:32:58.246Z" }, ] [[package]] @@ -4727,7 +4727,7 @@ wheels = [ [[package]] name = "typer" -version = "0.24.1" +version = "0.23.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -4735,9 +4735,9 @@ dependencies = [ { name = "rich" }, { name = "shellingham" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/07/b822e1b307d40e263e8253d2384cf98c51aa2368cc7ba9a07e523a1d964b/typer-0.23.1.tar.gz", hash = "sha256:2070374e4d31c83e7b61362fd859aa683576432fd5b026b060ad6b4cd3b86134", size = 120047, upload-time = "2026-02-13T10:04:30.984Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/9b286ab899c008c2cb05e8be99814807e7fbbd33f0c0c960470826e5ac82/typer-0.23.1-py3-none-any.whl", hash = "sha256:3291ad0d3c701cbf522012faccfbb29352ff16ad262db2139e6b01f15781f14e", size = 56813, upload-time = "2026-02-13T10:04:32.008Z" }, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 12ac017d84..a0b5ce4d2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] authors = [{ name = "TryCua", email = "gh@trycua.com" }] dependencies = [ - "openai<1.100.0", + "openai>=2.24.0,<3", "anthropic>=0.67.0", ] description = "Cua (Computer Use Agent) mono-repo" @@ -38,12 +38,12 @@ docs = [ "mkdocs>=1.5.0", ] docs-scripts = [ - "crawl4ai>=0.4.0", + "crawl4ai>=0.8.0", "playwright>=1.40.0", "lancedb>=0.4.0", "sentence-transformers>=2.2.0", "pyarrow>=14.0.1", - "fastmcp>=2.14.0", + "fastmcp>=3.2.0", "pydantic>=2.0.0", "pandas>=2.0.0", "markdown-it-py>=3.0.0", diff --git a/uv.lock b/uv.lock index e4d3a00e20..421653b25f 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.12, <3.14" resolution-markers = [ "python_full_version >= '3.13' and sys_platform == 'darwin'", @@ -88,6 +88,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/7c/0cf5e5008d1675183f346beffa030209ee0f640db086e39b484b53608c43/aiodocker-0.25.0-py3-none-any.whl", hash = "sha256:0e11f39360cc69dd0a27621f15ffbf42010fb146628920e9aa8de449a9fcb0c5", size = 46401, upload-time = "2025-12-20T04:51:27.177Z" }, ] +[[package]] +name = "aiofile" +version = "3.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "caio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/e2/d7cb819de8df6b5c1968a2756c3cb4122d4fa2b8fc768b53b7c9e5edb646/aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b", size = 17943, upload-time = "2024-10-08T10:39:35.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/25/da1f0b4dd970e52bf5a36c204c107e11a0c6d3ed195eba0bfbc664c312b2/aiofile-3.9.0-py3-none-any.whl", hash = "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa", size = 19539, upload-time = "2024-10-08T10:39:32.955Z" }, +] + [[package]] name = "aiofiles" version = "24.1.0" @@ -108,7 +120,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.13.2" +version = "3.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -119,42 +131,42 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/ce/3b83ebba6b3207a7135e5fcaba49706f8a4b6008153b4e30540c982fae26/aiohttp-3.13.2.tar.gz", hash = "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca", size = 7837994, upload-time = "2025-10-28T20:59:39.937Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/29/9b/01f00e9856d0a73260e86dd8ed0c2234a466c5c1712ce1c281548df39777/aiohttp-3.13.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b1e56bab2e12b2b9ed300218c351ee2a3d8c8fdab5b1ec6193e11a817767e47b", size = 737623, upload-time = "2025-10-28T20:56:30.797Z" }, - { url = "https://files.pythonhosted.org/packages/5a/1b/4be39c445e2b2bd0aab4ba736deb649fabf14f6757f405f0c9685019b9e9/aiohttp-3.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:364e25edaabd3d37b1db1f0cbcee8c73c9a3727bfa262b83e5e4cf3489a2a9dc", size = 492664, upload-time = "2025-10-28T20:56:32.708Z" }, - { url = "https://files.pythonhosted.org/packages/28/66/d35dcfea8050e131cdd731dff36434390479b4045a8d0b9d7111b0a968f1/aiohttp-3.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c5c94825f744694c4b8db20b71dba9a257cd2ba8e010a803042123f3a25d50d7", size = 491808, upload-time = "2025-10-28T20:56:34.57Z" }, - { url = "https://files.pythonhosted.org/packages/00/29/8e4609b93e10a853b65f8291e64985de66d4f5848c5637cddc70e98f01f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba2715d842ffa787be87cbfce150d5e88c87a98e0b62e0f5aa489169a393dbbb", size = 1738863, upload-time = "2025-10-28T20:56:36.377Z" }, - { url = "https://files.pythonhosted.org/packages/9d/fa/4ebdf4adcc0def75ced1a0d2d227577cd7b1b85beb7edad85fcc87693c75/aiohttp-3.13.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:585542825c4bc662221fb257889e011a5aa00f1ae4d75d1d246a5225289183e3", size = 1700586, upload-time = "2025-10-28T20:56:38.034Z" }, - { url = "https://files.pythonhosted.org/packages/da/04/73f5f02ff348a3558763ff6abe99c223381b0bace05cd4530a0258e52597/aiohttp-3.13.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39d02cb6025fe1aabca329c5632f48c9532a3dabccd859e7e2f110668972331f", size = 1768625, upload-time = "2025-10-28T20:56:39.75Z" }, - { url = "https://files.pythonhosted.org/packages/f8/49/a825b79ffec124317265ca7d2344a86bcffeb960743487cb11988ffb3494/aiohttp-3.13.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e67446b19e014d37342f7195f592a2a948141d15a312fe0e700c2fd2f03124f6", size = 1867281, upload-time = "2025-10-28T20:56:41.471Z" }, - { url = "https://files.pythonhosted.org/packages/b9/48/adf56e05f81eac31edcfae45c90928f4ad50ef2e3ea72cb8376162a368f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4356474ad6333e41ccefd39eae869ba15a6c5299c9c01dfdcfdd5c107be4363e", size = 1752431, upload-time = "2025-10-28T20:56:43.162Z" }, - { url = "https://files.pythonhosted.org/packages/30/ab/593855356eead019a74e862f21523db09c27f12fd24af72dbc3555b9bfd9/aiohttp-3.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeacf451c99b4525f700f078becff32c32ec327b10dcf31306a8a52d78166de7", size = 1562846, upload-time = "2025-10-28T20:56:44.85Z" }, - { url = "https://files.pythonhosted.org/packages/39/0f/9f3d32271aa8dc35036e9668e31870a9d3b9542dd6b3e2c8a30931cb27ae/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8a9b889aeabd7a4e9af0b7f4ab5ad94d42e7ff679aaec6d0db21e3b639ad58d", size = 1699606, upload-time = "2025-10-28T20:56:46.519Z" }, - { url = "https://files.pythonhosted.org/packages/2c/3c/52d2658c5699b6ef7692a3f7128b2d2d4d9775f2a68093f74bca06cf01e1/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fa89cb11bc71a63b69568d5b8a25c3ca25b6d54c15f907ca1c130d72f320b76b", size = 1720663, upload-time = "2025-10-28T20:56:48.528Z" }, - { url = "https://files.pythonhosted.org/packages/9b/d4/8f8f3ff1fb7fb9e3f04fcad4e89d8a1cd8fc7d05de67e3de5b15b33008ff/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8aa7c807df234f693fed0ecd507192fc97692e61fee5702cdc11155d2e5cadc8", size = 1737939, upload-time = "2025-10-28T20:56:50.77Z" }, - { url = "https://files.pythonhosted.org/packages/03/d3/ddd348f8a27a634daae39a1b8e291ff19c77867af438af844bf8b7e3231b/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9eb3e33fdbe43f88c3c75fa608c25e7c47bbd80f48d012763cb67c47f39a7e16", size = 1555132, upload-time = "2025-10-28T20:56:52.568Z" }, - { url = "https://files.pythonhosted.org/packages/39/b8/46790692dc46218406f94374903ba47552f2f9f90dad554eed61bfb7b64c/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9434bc0d80076138ea986833156c5a48c9c7a8abb0c96039ddbb4afc93184169", size = 1764802, upload-time = "2025-10-28T20:56:54.292Z" }, - { url = "https://files.pythonhosted.org/packages/ba/e4/19ce547b58ab2a385e5f0b8aa3db38674785085abcf79b6e0edd1632b12f/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff15c147b2ad66da1f2cbb0622313f2242d8e6e8f9b79b5206c84523a4473248", size = 1719512, upload-time = "2025-10-28T20:56:56.428Z" }, - { url = "https://files.pythonhosted.org/packages/70/30/6355a737fed29dcb6dfdd48682d5790cb5eab050f7b4e01f49b121d3acad/aiohttp-3.13.2-cp312-cp312-win32.whl", hash = "sha256:27e569eb9d9e95dbd55c0fc3ec3a9335defbf1d8bc1d20171a49f3c4c607b93e", size = 426690, upload-time = "2025-10-28T20:56:58.736Z" }, - { url = "https://files.pythonhosted.org/packages/0a/0d/b10ac09069973d112de6ef980c1f6bb31cb7dcd0bc363acbdad58f927873/aiohttp-3.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:8709a0f05d59a71f33fd05c17fc11fcb8c30140506e13c2f5e8ee1b8964e1b45", size = 453465, upload-time = "2025-10-28T20:57:00.795Z" }, - { url = "https://files.pythonhosted.org/packages/bf/78/7e90ca79e5aa39f9694dcfd74f4720782d3c6828113bb1f3197f7e7c4a56/aiohttp-3.13.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be", size = 732139, upload-time = "2025-10-28T20:57:02.455Z" }, - { url = "https://files.pythonhosted.org/packages/db/ed/1f59215ab6853fbaa5c8495fa6cbc39edfc93553426152b75d82a5f32b76/aiohttp-3.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742", size = 490082, upload-time = "2025-10-28T20:57:04.784Z" }, - { url = "https://files.pythonhosted.org/packages/68/7b/fe0fe0f5e05e13629d893c760465173a15ad0039c0a5b0d0040995c8075e/aiohttp-3.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293", size = 489035, upload-time = "2025-10-28T20:57:06.894Z" }, - { url = "https://files.pythonhosted.org/packages/d2/04/db5279e38471b7ac801d7d36a57d1230feeee130bbe2a74f72731b23c2b1/aiohttp-3.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1237c1375eaef0db4dcd7c2559f42e8af7b87ea7d295b118c60c36a6e61cb811", size = 1720387, upload-time = "2025-10-28T20:57:08.685Z" }, - { url = "https://files.pythonhosted.org/packages/31/07/8ea4326bd7dae2bd59828f69d7fdc6e04523caa55e4a70f4a8725a7e4ed2/aiohttp-3.13.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96581619c57419c3d7d78703d5b78c1e5e5fc0172d60f555bdebaced82ded19a", size = 1688314, upload-time = "2025-10-28T20:57:10.693Z" }, - { url = "https://files.pythonhosted.org/packages/48/ab/3d98007b5b87ffd519d065225438cc3b668b2f245572a8cb53da5dd2b1bc/aiohttp-3.13.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2713a95b47374169409d18103366de1050fe0ea73db358fc7a7acb2880422d4", size = 1756317, upload-time = "2025-10-28T20:57:12.563Z" }, - { url = "https://files.pythonhosted.org/packages/97/3d/801ca172b3d857fafb7b50c7c03f91b72b867a13abca982ed6b3081774ef/aiohttp-3.13.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:228a1cd556b3caca590e9511a89444925da87d35219a49ab5da0c36d2d943a6a", size = 1858539, upload-time = "2025-10-28T20:57:14.623Z" }, - { url = "https://files.pythonhosted.org/packages/f7/0d/4764669bdf47bd472899b3d3db91fffbe925c8e3038ec591a2fd2ad6a14d/aiohttp-3.13.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6cde5fba8d7d8c6ac963dbb0256a9854e9fafff52fbcc58fdf819357892c3e", size = 1739597, upload-time = "2025-10-28T20:57:16.399Z" }, - { url = "https://files.pythonhosted.org/packages/c4/52/7bd3c6693da58ba16e657eb904a5b6decfc48ecd06e9ac098591653b1566/aiohttp-3.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2bef8237544f4e42878c61cef4e2839fee6346dc60f5739f876a9c50be7fcdb", size = 1555006, upload-time = "2025-10-28T20:57:18.288Z" }, - { url = "https://files.pythonhosted.org/packages/48/30/9586667acec5993b6f41d2ebcf96e97a1255a85f62f3c653110a5de4d346/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:16f15a4eac3bc2d76c45f7ebdd48a65d41b242eb6c31c2245463b40b34584ded", size = 1683220, upload-time = "2025-10-28T20:57:20.241Z" }, - { url = "https://files.pythonhosted.org/packages/71/01/3afe4c96854cfd7b30d78333852e8e851dceaec1c40fd00fec90c6402dd2/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:bb7fb776645af5cc58ab804c58d7eba545a97e047254a52ce89c157b5af6cd0b", size = 1712570, upload-time = "2025-10-28T20:57:22.253Z" }, - { url = "https://files.pythonhosted.org/packages/11/2c/22799d8e720f4697a9e66fd9c02479e40a49de3de2f0bbe7f9f78a987808/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1b4951125ec10c70802f2cb09736c895861cd39fd9dcb35107b4dc8ae6220b8", size = 1733407, upload-time = "2025-10-28T20:57:24.37Z" }, - { url = "https://files.pythonhosted.org/packages/34/cb/90f15dd029f07cebbd91f8238a8b363978b530cd128488085b5703683594/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:550bf765101ae721ee1d37d8095f47b1f220650f85fe1af37a90ce75bab89d04", size = 1550093, upload-time = "2025-10-28T20:57:26.257Z" }, - { url = "https://files.pythonhosted.org/packages/69/46/12dce9be9d3303ecbf4d30ad45a7683dc63d90733c2d9fe512be6716cd40/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fe91b87fc295973096251e2d25a811388e7d8adf3bd2b97ef6ae78bc4ac6c476", size = 1758084, upload-time = "2025-10-28T20:57:28.349Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c8/0932b558da0c302ffd639fc6362a313b98fdf235dc417bc2493da8394df7/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0c8e31cfcc4592cb200160344b2fb6ae0f9e4effe06c644b5a125d4ae5ebe23", size = 1716987, upload-time = "2025-10-28T20:57:30.233Z" }, - { url = "https://files.pythonhosted.org/packages/5d/8b/f5bd1a75003daed099baec373aed678f2e9b34f2ad40d85baa1368556396/aiohttp-3.13.2-cp313-cp313-win32.whl", hash = "sha256:0740f31a60848d6edb296a0df827473eede90c689b8f9f2a4cdde74889eb2254", size = 425859, upload-time = "2025-10-28T20:57:32.105Z" }, - { url = "https://files.pythonhosted.org/packages/5d/28/a8a9fc6957b2cee8902414e41816b5ab5536ecf43c3b1843c10e82c559b2/aiohttp-3.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:a88d13e7ca367394908f8a276b89d04a3652044612b9a408a0bb22a5ed976a1a", size = 452192, upload-time = "2025-10-28T20:57:34.166Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/45/4a/064321452809dae953c1ed6e017504e72551a26b6f5708a5a80e4bf556ff/aiohttp-3.13.4.tar.gz", hash = "sha256:d97a6d09c66087890c2ab5d49069e1e570583f7ac0314ecf98294c1b6aaebd38", size = 7859748, upload-time = "2026-03-28T17:19:40.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/bd/ede278648914cabbabfdf95e436679b5d4156e417896a9b9f4587169e376/aiohttp-3.13.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ee62d4471ce86b108b19c3364db4b91180d13fe3510144872d6bad5401957360", size = 752158, upload-time = "2026-03-28T17:16:06.901Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/581c053253c07b480b03785196ca5335e3c606a37dc73e95f6527f1591fe/aiohttp-3.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c0fd8f41b54b58636402eb493afd512c23580456f022c1ba2db0f810c959ed0d", size = 501037, upload-time = "2026-03-28T17:16:08.82Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/a5ede193c08f13cc42c0a5b50d1e246ecee9115e4cf6e900d8dbd8fd6acb/aiohttp-3.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4baa48ce49efd82d6b1a0be12d6a36b35e5594d1dd42f8bfba96ea9f8678b88c", size = 501556, upload-time = "2026-03-28T17:16:10.63Z" }, + { url = "https://files.pythonhosted.org/packages/d6/10/88ff67cd48a6ec36335b63a640abe86135791544863e0cfe1f065d6cef7a/aiohttp-3.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d738ebab9f71ee652d9dbd0211057690022201b11197f9a7324fd4dba128aa97", size = 1757314, upload-time = "2026-03-28T17:16:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/8b/15/fdb90a5cf5a1f52845c276e76298c75fbbcc0ac2b4a86551906d54529965/aiohttp-3.13.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0ce692c3468fa831af7dceed52edf51ac348cebfc8d3feb935927b63bd3e8576", size = 1731819, upload-time = "2026-03-28T17:16:14.558Z" }, + { url = "https://files.pythonhosted.org/packages/ec/df/28146785a007f7820416be05d4f28cc207493efd1e8c6c1068e9bdc29198/aiohttp-3.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e08abcfe752a454d2cb89ff0c08f2d1ecd057ae3e8cc6d84638de853530ebab", size = 1793279, upload-time = "2026-03-28T17:16:16.594Z" }, + { url = "https://files.pythonhosted.org/packages/10/47/689c743abf62ea7a77774d5722f220e2c912a77d65d368b884d9779ef41b/aiohttp-3.13.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5977f701b3fff36367a11087f30ea73c212e686d41cd363c50c022d48b011d8d", size = 1891082, upload-time = "2026-03-28T17:16:18.71Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/f7f4f318c7e58c23b761c9b13b9a3c9b394e0f9d5d76fbc6622fa98509f6/aiohttp-3.13.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54203e10405c06f8b6020bd1e076ae0fe6c194adcee12a5a78af3ffa3c57025e", size = 1773938, upload-time = "2026-03-28T17:16:21.125Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/f207cb3121852c989586a6fc16ff854c4fcc8651b86c5d3bd1fc83057650/aiohttp-3.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:358a6af0145bc4dda037f13167bef3cce54b132087acc4c295c739d05d16b1c3", size = 1579548, upload-time = "2026-03-28T17:16:23.588Z" }, + { url = "https://files.pythonhosted.org/packages/6c/58/e1289661a32161e24c1fe479711d783067210d266842523752869cc1d9c2/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:898ea1850656d7d61832ef06aa9846ab3ddb1621b74f46de78fbc5e1a586ba83", size = 1714669, upload-time = "2026-03-28T17:16:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/96/0a/3e86d039438a74a86e6a948a9119b22540bae037d6ba317a042ae3c22711/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7bc30cceb710cf6a44e9617e43eebb6e3e43ad855a34da7b4b6a73537d8a6763", size = 1754175, upload-time = "2026-03-28T17:16:28.18Z" }, + { url = "https://files.pythonhosted.org/packages/f4/30/e717fc5df83133ba467a560b6d8ef20197037b4bb5d7075b90037de1018e/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4a31c0c587a8a038f19a4c7e60654a6c899c9de9174593a13e7cc6e15ff271f9", size = 1762049, upload-time = "2026-03-28T17:16:30.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/28/8f7a2d4492e336e40005151bdd94baf344880a4707573378579f833a64c1/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2062f675f3fe6e06d6113eb74a157fb9df58953ffed0cdb4182554b116545758", size = 1570861, upload-time = "2026-03-28T17:16:32.953Z" }, + { url = "https://files.pythonhosted.org/packages/78/45/12e1a3d0645968b1c38de4b23fdf270b8637735ea057d4f84482ff918ad9/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d1ba8afb847ff80626d5e408c1fdc99f942acc877d0702fe137015903a220a9", size = 1790003, upload-time = "2026-03-28T17:16:35.468Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/60374e18d590de16dcb39d6ff62f39c096c1b958e6f37727b5870026ea30/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b08149419994cdd4d5eecf7fd4bc5986b5a9380285bcd01ab4c0d6bfca47b79d", size = 1737289, upload-time = "2026-03-28T17:16:38.187Z" }, + { url = "https://files.pythonhosted.org/packages/02/bf/535e58d886cfbc40a8b0013c974afad24ef7632d645bca0b678b70033a60/aiohttp-3.13.4-cp312-cp312-win32.whl", hash = "sha256:fc432f6a2c4f720180959bc19aa37259651c1a4ed8af8afc84dd41c60f15f791", size = 434185, upload-time = "2026-03-28T17:16:40.735Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1a/d92e3325134ebfff6f4069f270d3aac770d63320bd1fcd0eca023e74d9a8/aiohttp-3.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:6148c9ae97a3e8bff9a1fc9c757fa164116f86c100468339730e717590a3fb77", size = 461285, upload-time = "2026-03-28T17:16:42.713Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ac/892f4162df9b115b4758d615f32ec63d00f3084c705ff5526630887b9b42/aiohttp-3.13.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:63dd5e5b1e43b8fb1e91b79b7ceba1feba588b317d1edff385084fcc7a0a4538", size = 745744, upload-time = "2026-03-28T17:16:44.67Z" }, + { url = "https://files.pythonhosted.org/packages/97/a9/c5b87e4443a2f0ea88cb3000c93a8fdad1ee63bffc9ded8d8c8e0d66efc6/aiohttp-3.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:746ac3cc00b5baea424dacddea3ec2c2702f9590de27d837aa67004db1eebc6e", size = 498178, upload-time = "2026-03-28T17:16:46.766Z" }, + { url = "https://files.pythonhosted.org/packages/94/42/07e1b543a61250783650df13da8ddcdc0d0a5538b2bd15cef6e042aefc61/aiohttp-3.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bda8f16ea99d6a6705e5946732e48487a448be874e54a4f73d514660ff7c05d3", size = 498331, upload-time = "2026-03-28T17:16:48.9Z" }, + { url = "https://files.pythonhosted.org/packages/20/d6/492f46bf0328534124772d0cf58570acae5b286ea25006900650f69dae0e/aiohttp-3.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b061e7b5f840391e3f64d0ddf672973e45c4cfff7a0feea425ea24e51530fc2", size = 1744414, upload-time = "2026-03-28T17:16:50.968Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/e02627b2683f68051246215d2d62b2d2f249ff7a285e7a858dc47d6b6a14/aiohttp-3.13.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b252e8d5cd66184b570d0d010de742736e8a4fab22c58299772b0c5a466d4b21", size = 1719226, upload-time = "2026-03-28T17:16:53.173Z" }, + { url = "https://files.pythonhosted.org/packages/7b/6c/5d0a3394dd2b9f9aeba6e1b6065d0439e4b75d41f1fb09a3ec010b43552b/aiohttp-3.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20af8aad61d1803ff11152a26146d8d81c266aa8c5aa9b4504432abb965c36a0", size = 1782110, upload-time = "2026-03-28T17:16:55.362Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2d/c20791e3437700a7441a7edfb59731150322424f5aadf635602d1d326101/aiohttp-3.13.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:13a5cc924b59859ad2adb1478e31f410a7ed46e92a2a619d6d1dd1a63c1a855e", size = 1884809, upload-time = "2026-03-28T17:16:57.734Z" }, + { url = "https://files.pythonhosted.org/packages/c8/94/d99dbfbd1924a87ef643833932eb2a3d9e5eee87656efea7d78058539eff/aiohttp-3.13.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:534913dfb0a644d537aebb4123e7d466d94e3be5549205e6a31f72368980a81a", size = 1764938, upload-time = "2026-03-28T17:17:00.221Z" }, + { url = "https://files.pythonhosted.org/packages/49/61/3ce326a1538781deb89f6cf5e094e2029cd308ed1e21b2ba2278b08426f6/aiohttp-3.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:320e40192a2dcc1cf4b5576936e9652981ab596bf81eb309535db7e2f5b5672f", size = 1570697, upload-time = "2026-03-28T17:17:02.985Z" }, + { url = "https://files.pythonhosted.org/packages/b6/77/4ab5a546857bb3028fbaf34d6eea180267bdab022ee8b1168b1fcde4bfdd/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9e587fcfce2bcf06526a43cb705bdee21ac089096f2e271d75de9c339db3100c", size = 1702258, upload-time = "2026-03-28T17:17:05.28Z" }, + { url = "https://files.pythonhosted.org/packages/79/63/d8f29021e39bc5af8e5d5e9da1b07976fb9846487a784e11e4f4eeda4666/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9eb9c2eea7278206b5c6c1441fdd9dc420c278ead3f3b2cc87f9b693698cc500", size = 1740287, upload-time = "2026-03-28T17:17:07.712Z" }, + { url = "https://files.pythonhosted.org/packages/55/3a/cbc6b3b124859a11bc8055d3682c26999b393531ef926754a3445b99dfef/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:29be00c51972b04bf9d5c8f2d7f7314f48f96070ca40a873a53056e652e805f7", size = 1753011, upload-time = "2026-03-28T17:17:10.053Z" }, + { url = "https://files.pythonhosted.org/packages/e0/30/836278675205d58c1368b21520eab9572457cf19afd23759216c04483048/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90c06228a6c3a7c9f776fe4fc0b7ff647fffd3bed93779a6913c804ae00c1073", size = 1566359, upload-time = "2026-03-28T17:17:12.433Z" }, + { url = "https://files.pythonhosted.org/packages/50/b4/8032cc9b82d17e4277704ba30509eaccb39329dc18d6a35f05e424439e32/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a533ec132f05fd9a1d959e7f34184cd7d5e8511584848dab85faefbaac573069", size = 1785537, upload-time = "2026-03-28T17:17:14.721Z" }, + { url = "https://files.pythonhosted.org/packages/17/7d/5873e98230bde59f493bf1f7c3e327486a4b5653fa401144704df5d00211/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1c946f10f413836f82ea4cfb90200d2a59578c549f00857e03111cf45ad01ca5", size = 1740752, upload-time = "2026-03-28T17:17:17.387Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f2/13e46e0df051494d7d3c68b7f72d071f48c384c12716fc294f75d5b1a064/aiohttp-3.13.4-cp313-cp313-win32.whl", hash = "sha256:48708e2706106da6967eff5908c78ca3943f005ed6bcb75da2a7e4da94ef8c70", size = 433187, upload-time = "2026-03-28T17:17:19.523Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c0/649856ee655a843c8f8664592cfccb73ac80ede6a8c8db33a25d810c12db/aiohttp-3.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:74a2eb058da44fa3a877a49e2095b591d4913308bb424c418b77beb160c55ce3", size = 459778, upload-time = "2026-03-28T17:17:21.964Z" }, ] [[package]] @@ -412,14 +424,15 @@ wheels = [ [[package]] name = "authlib" -version = "1.6.6" +version = "1.7.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, + { name = "joserfc" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/9b/b1661026ff24bc641b76b78c5222d614776b0c085bcfdac9bd15a1cb4b35/authlib-1.6.6.tar.gz", hash = "sha256:45770e8e056d0f283451d9996fbb59b70d45722b45d854d58f32878d0a40c38e", size = 164894, upload-time = "2025-12-12T08:01:41.464Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/98/7d93f30d029643c0275dbc0bd6d5a6f670661ee6c9a94d93af7ab4887600/authlib-1.7.2.tar.gz", hash = "sha256:2cea25fefcd4e7173bdf1372c0afc265c8034b23a8cd5dcb6a9164b826c64231", size = 176511, upload-time = "2026-05-06T08:10:23.116Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/51/321e821856452f7386c4e9df866f196720b1ad0c5ea1623ea7399969ae3b/authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd", size = 244005, upload-time = "2025-12-12T08:01:40.209Z" }, + { url = "https://files.pythonhosted.org/packages/fb/95/adcb68e20c34162e9135f370d6e31737719c2b6f94bc953fe7ed1f10fe21/authlib-1.7.2-py2.py3-none-any.whl", hash = "sha256:3e1faedc9d87e7d56a164eca3ccb6ace0d61b94abe83e92242f8dc8bba9b4a9f", size = 259548, upload-time = "2026-05-06T08:10:21.436Z" }, ] [[package]] @@ -659,6 +672,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/fc/1d7b80d0eb7b714984ce40efc78859c022cd930e402f599d8ca9e39c78a4/cachetools-6.2.4-py3-none-any.whl", hash = "sha256:69a7a52634fed8b8bf6e24a050fb60bff1c9bd8f6d24572b99c32d4e71e62a51", size = 11551, upload-time = "2025-12-15T18:24:52.332Z" }, ] +[[package]] +name = "caio" +version = "0.9.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db339a1df8bd1ae49d146fcea9d6a5c40e3a80aaeb38d/caio-0.9.25.tar.gz", hash = "sha256:16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10", size = 26781, upload-time = "2025-12-26T15:21:36.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/25/79c98ebe12df31548ba4eaf44db11b7cad6b3e7b4203718335620939083c/caio-0.9.25-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044", size = 36983, upload-time = "2025-12-26T15:21:36.075Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2b/21288691f16d479945968a0a4f2856818c1c5be56881d51d4dac9b255d26/caio-0.9.25-cp312-cp312-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64", size = 82012, upload-time = "2025-12-26T15:22:20.983Z" }, + { url = "https://files.pythonhosted.org/packages/03/c4/8a1b580875303500a9c12b9e0af58cb82e47f5bcf888c2457742a138273c/caio-0.9.25-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:4fa69eba47e0f041b9d4f336e2ad40740681c43e686b18b191b6c5f4c5544bfb", size = 81502, upload-time = "2026-03-04T22:08:22.381Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/0fe770b8ffc8362c48134d1592d653a81a3d8748d764bec33864db36319d/caio-0.9.25-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:6bebf6f079f1341d19f7386db9b8b1f07e8cc15ae13bfdaff573371ba0575d69", size = 80200, upload-time = "2026-03-04T22:08:23.382Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/5e6ff127e6f62c9f15d989560435c642144aa4210882f9494204bc892305/caio-0.9.25-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451", size = 36979, upload-time = "2025-12-26T15:21:35.484Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9f/f21af50e72117eb528c422d4276cbac11fb941b1b812b182e0a9c70d19c5/caio-0.9.25-cp313-cp313-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6", size = 81900, upload-time = "2025-12-26T15:22:21.919Z" }, + { url = "https://files.pythonhosted.org/packages/9c/12/c39ae2a4037cb10ad5eb3578eb4d5f8c1a2575c62bba675f3406b7ef0824/caio-0.9.25-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:1a177d4777141b96f175fe2c37a3d96dec7911ed9ad5f02bac38aaa1c936611f", size = 81523, upload-time = "2026-03-04T22:08:25.187Z" }, + { url = "https://files.pythonhosted.org/packages/22/59/f8f2e950eb4f1a5a3883e198dca514b9d475415cb6cd7b78b9213a0dd45a/caio-0.9.25-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:9ed3cfb28c0e99fec5e208c934e5c157d0866aa9c32aa4dc5e9b6034af6286b7", size = 80243, upload-time = "2026-03-04T22:08:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" }, +] + [[package]] name = "cbor2" version = "5.8.0" @@ -787,14 +817,14 @@ wheels = [ [[package]] name = "click" -version = "8.2.1" +version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, ] [[package]] @@ -809,15 +839,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl", hash = "sha256:a43e394b528d52112af599f2fc9e4b7cf3c15f94e53581f74fa6867e68c91756", size = 4273, upload-time = "2022-03-13T11:10:17.594Z" }, ] -[[package]] -name = "cloudpickle" -version = "3.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, -] - [[package]] name = "clr-loader" version = "0.2.9" @@ -963,7 +984,7 @@ wheels = [ [[package]] name = "crawl4ai" -version = "0.7.8" +version = "0.8.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiofiles" }, @@ -980,13 +1001,13 @@ dependencies = [ { name = "httpx", extra = ["http2"] }, { name = "humanize" }, { name = "lark" }, - { name = "litellm" }, { name = "lxml" }, { name = "nltk" }, { name = "numpy" }, { name = "patchright" }, { name = "pillow" }, { name = "playwright" }, + { name = "playwright-stealth" }, { name = "psutil" }, { name = "pydantic" }, { name = "pyopenssl" }, @@ -997,12 +1018,12 @@ dependencies = [ { name = "rich" }, { name = "shapely" }, { name = "snowballstemmer" }, - { name = "tf-playwright-stealth" }, + { name = "unclecode-litellm" }, { name = "xxhash" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d6/8f/08133fcf6f4ae41aff02af8c5353af831e81bf925bc667ef2f8653abd7d2/crawl4ai-0.7.8.tar.gz", hash = "sha256:ed0189ca19ccc1dad349a37565d9dcbfed7897650e44d7c6ae25a37eef5ab44b", size = 462833, upload-time = "2025-12-09T08:49:35.845Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/96/7525bd2e1f8f2f2924f350b0432481d673c3fb43e4d60f7d151e4137b8a6/crawl4ai-0.8.6.tar.gz", hash = "sha256:2a2afab05ae021435fe025cfc5732f5dcebcefd7ff9a529e41050642ee954316", size = 578962, upload-time = "2026-03-24T15:07:51.706Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/b3/d2cc38bf9a3a285de5d2da6057e0a4809e41cd4344e2e88a00de19b51c7a/crawl4ai-0.7.8-py3-none-any.whl", hash = "sha256:a25a108e3e027022e98a6ab447d8261676c5e83717fa712502718ac47456b1dd", size = 438138, upload-time = "2025-12-09T08:49:34.129Z" }, + { url = "https://files.pythonhosted.org/packages/76/e2/aa851e2f0248f0b19c63b0fe11f6bdc2ca64900924d399af368dc28f2a20/crawl4ai-0.8.6-py3-none-any.whl", hash = "sha256:57e127e8113640d8358705b231111d3cd9cc4ab05d44705e724cebc4a383e09c", size = 501931, upload-time = "2026-03-24T15:07:50.121Z" }, ] [[package]] @@ -1057,7 +1078,7 @@ wheels = [ [[package]] name = "cua-agent" -version = "0.7.39" +version = "0.8.2" source = { editable = "libs/python/agent" } dependencies = [ { name = "aiohttp" }, @@ -1111,7 +1132,7 @@ gemini = [ glm45v-hf = [ { name = "accelerate" }, { name = "torch" }, - { name = "transformers-v4-55-0-glm-4-5v-preview" }, + { name = "transformers" }, ] hud = [ { name = "hud-python" }, @@ -1182,7 +1203,7 @@ requires-dist = [ { name = "gradio", marker = "extra == 'ui'", specifier = ">=6.0.0" }, { name = "httpx", specifier = ">=0.27.0" }, { name = "hud-python", marker = "extra == 'hud'", specifier = "==0.4.52" }, - { name = "litellm", specifier = "==1.80.0" }, + { name = "litellm", specifier = ">=1.83.7,<1.84" }, { name = "mlx-vlm", marker = "sys_platform == 'darwin' and extra == 'all'", specifier = ">=0.1.27" }, { name = "mlx-vlm", marker = "sys_platform == 'darwin' and extra == 'uitars-mlx'", specifier = ">=0.1.27" }, { name = "pillow", specifier = ">=10.0.0" }, @@ -1211,11 +1232,11 @@ requires-dist = [ { name = "torch", marker = "extra == 'opencua-hf'" }, { name = "torch", marker = "extra == 'uitars-hf'" }, { name = "transformers", marker = "extra == 'all'", specifier = ">=4.55.0" }, + { name = "transformers", marker = "extra == 'glm45v-hf'", specifier = ">=4.57.6,<5" }, { name = "transformers", marker = "extra == 'internvl-hf'", specifier = ">=4.55.0" }, { name = "transformers", marker = "extra == 'moondream3'", specifier = ">=4.55.0" }, { name = "transformers", marker = "extra == 'opencua-hf'", specifier = ">=4.53.0" }, { name = "transformers", marker = "extra == 'uitars-hf'", specifier = ">=4.54.0" }, - { name = "transformers-v4-55-0-glm-4-5v-preview", marker = "extra == 'glm45v-hf'" }, { name = "typing-extensions", specifier = ">=4.12.2" }, { name = "yaspin", marker = "extra == 'all'", specifier = ">=3.1.0" }, { name = "yaspin", marker = "extra == 'cli'", specifier = ">=3.1.0" }, @@ -1269,7 +1290,7 @@ requires-dist = [ [[package]] name = "cua-computer" -version = "0.5.17" +version = "0.5.18" source = { editable = "libs/python/computer" } dependencies = [ { name = "aiohttp" }, @@ -1313,7 +1334,7 @@ provides-extras = ["lume", "lumier", "ui", "all"] [[package]] name = "cua-computer-server" -version = "0.3.28" +version = "0.3.39" source = { editable = "libs/python/computer-server" } dependencies = [ { name = "aiohttp" }, @@ -1361,7 +1382,7 @@ requires-dist = [ { name = "cua-auto", editable = "libs/python/cua-auto" }, { name = "cua-core", editable = "libs/python/core" }, { name = "fastapi", specifier = ">=0.111.0" }, - { name = "fastmcp", specifier = ">=2.0,<3" }, + { name = "fastmcp", specifier = ">=3.2.0,<4" }, { name = "grpcio", specifier = "==1.78.0" }, { name = "pillow", specifier = ">=10.2.0" }, { name = "playwright", specifier = ">=1.40.0" }, @@ -1389,7 +1410,7 @@ provides-extras = ["macos", "linux", "windows", "vnc"] [[package]] name = "cua-core" -version = "0.1.19" +version = "0.3.1" source = { editable = "libs/python/core" } dependencies = [ { name = "posthog" }, @@ -1540,7 +1561,7 @@ test = [ [package.metadata] requires-dist = [ { name = "anthropic", specifier = ">=0.67.0" }, - { name = "openai", specifier = "<1.100.0" }, + { name = "openai", specifier = ">=2.24.0,<3" }, ] [package.metadata.requires-dev] @@ -1567,8 +1588,8 @@ docs = [ { name = "mkdocs-material", specifier = ">=9.2.0" }, ] docs-scripts = [ - { name = "crawl4ai", specifier = ">=0.4.0" }, - { name = "fastmcp", specifier = ">=2.14.0" }, + { name = "crawl4ai", specifier = ">=0.8.0" }, + { name = "fastmcp", specifier = ">=3.2.0" }, { name = "gitpython", specifier = ">=3.1.43" }, { name = "griffe", specifier = ">=2.0.0" }, { name = "lancedb", specifier = ">=0.4.0" }, @@ -1711,15 +1732,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668, upload-time = "2025-04-16T00:41:47.671Z" }, ] -[[package]] -name = "diskcache" -version = "5.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, -] - [[package]] name = "distlib" version = "0.4.0" @@ -1877,14 +1889,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] -[[package]] -name = "fake-http-header" -version = "0.3.5" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/0b/2849c87d9f13766e29c0a2f4d31681aa72e035016b251ab19d99bde7b592/fake_http_header-0.3.5-py3-none-any.whl", hash = "sha256:cd05f4bebf1b7e38b5f5c03d7fb820c0c17e87d9614fbee0afa39c32c7a2ad3c", size = 14938, upload-time = "2024-10-15T07:27:10.671Z" }, -] - [[package]] name = "fake-useragent" version = "2.2.0" @@ -1894,24 +1898,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/37/b3ea9cd5558ff4cb51957caca2193981c6b0ff30bd0d2630ac62505d99d0/fake_useragent-2.2.0-py3-none-any.whl", hash = "sha256:67f35ca4d847b0d298187443aaf020413746e56acd985a611908c73dba2daa24", size = 161695, upload-time = "2025-04-14T15:32:17.732Z" }, ] -[[package]] -name = "fakeredis" -version = "2.33.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "redis" }, - { name = "sortedcontainers" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5f/f9/57464119936414d60697fcbd32f38909bb5688b616ae13de6e98384433e0/fakeredis-2.33.0.tar.gz", hash = "sha256:d7bc9a69d21df108a6451bbffee23b3eba432c21a654afc7ff2d295428ec5770", size = 175187, upload-time = "2025-12-16T19:45:52.269Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/78/a850fed8aeef96d4a99043c90b818b2ed5419cd5b24a4049fd7cfb9f1471/fakeredis-2.33.0-py3-none-any.whl", hash = "sha256:de535f3f9ccde1c56672ab2fdd6a8efbc4f2619fc2f1acc87b8737177d71c965", size = 119605, upload-time = "2025-12-16T19:45:51.08Z" }, -] - -[package.optional-dependencies] -lua = [ - { name = "lupa" }, -] - [[package]] name = "fastapi" version = "0.128.0" @@ -1938,29 +1924,35 @@ wheels = [ [[package]] name = "fastmcp" -version = "2.14.2" +version = "3.2.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "authlib" }, { name = "cyclopts" }, { name = "exceptiongroup" }, + { name = "griffelib" }, { name = "httpx" }, + { name = "jsonref" }, { name = "jsonschema-path" }, { name = "mcp" }, { name = "openapi-pydantic" }, + { name = "opentelemetry-api" }, + { name = "packaging" }, { name = "platformdirs" }, - { name = "py-key-value-aio", extra = ["disk", "keyring", "memory"] }, + { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] }, { name = "pydantic", extra = ["email"] }, - { name = "pydocket" }, { name = "pyperclip" }, { name = "python-dotenv" }, + { name = "pyyaml" }, { name = "rich" }, + { name = "uncalled-for" }, { name = "uvicorn" }, + { name = "watchfiles" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/1e/e3528227688c248283f6d86869b1e900563ffc223eff00f4f923d2750365/fastmcp-2.14.2.tar.gz", hash = "sha256:bd23d1b808b6f446444f10114dac468b11bfb9153ed78628f5619763d0cf573e", size = 8272966, upload-time = "2025-12-31T15:26:13.433Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/13/29544fbc6dfe45ea38046af0067311e0bad7acc7d1f2ad38bb08f2409fe2/fastmcp-3.2.4.tar.gz", hash = "sha256:083ecb75b44a4169e7fc0f632f94b781bdb0ff877c6b35b9877cbb566fd4d4d1", size = 28746127, upload-time = "2026-04-14T01:42:24.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/67/8456d39484fcb7afd0defed21918e773ed59a98b39e5b633328527c88367/fastmcp-2.14.2-py3-none-any.whl", hash = "sha256:e33cd622e1ebd5110af6a981804525b6cd41072e3c7d68268ed69ef3be651aca", size = 413279, upload-time = "2025-12-31T15:26:11.178Z" }, + { url = "https://files.pythonhosted.org/packages/cf/76/b310d52fa0e30d39bd937eb58ec2c1f1ea1b5f519f0575e9dd9612f01deb/fastmcp-3.2.4-py3-none-any.whl", hash = "sha256:e6c9c429171041455e47ab94bb3f83c4657622a0ec28922f6940053959bd58a9", size = 728599, upload-time = "2026-04-14T01:42:26.85Z" }, ] [[package]] @@ -2768,14 +2760,14 @@ wheels = [ [[package]] name = "importlib-metadata" -version = "8.7.1" +version = "8.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload-time = "2024-09-11T14:56:08.937Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload-time = "2024-09-11T14:56:07.019Z" }, ] [[package]] @@ -3074,6 +3066,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, ] +[[package]] +name = "joserfc" +version = "1.6.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/dc/5f768c2e391e9afabe5d18e3221346deb5fb6338565f1ccc9e7c6d7befdd/joserfc-1.6.5.tar.gz", hash = "sha256:1482a7db78fb4602e44ed89e51b599d052e091288c7c532c5b694e20149dec48", size = 231881, upload-time = "2026-05-06T04:58:13.408Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/3b/ad1cb22e75c963b1f07c8a2329bf47227ce7e4361df5eb2fb101b2ce33ef/joserfc-1.6.5-py3-none-any.whl", hash = "sha256:e9878a0f8243fe7b95e11fdda81374ca9f7a689e302751579d3dfdeec559675e", size = 70464, upload-time = "2026-05-06T04:58:11.668Z" }, +] + [[package]] name = "json5" version = "0.12.1" @@ -3139,7 +3143,7 @@ wheels = [ [[package]] name = "jsonschema" -version = "4.25.1" +version = "4.23.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -3147,9 +3151,9 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778, upload-time = "2024-07-08T18:40:05.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462, upload-time = "2024-07-08T18:40:00.165Z" }, ] [package.optional-dependencies] @@ -3160,7 +3164,6 @@ format-nongpl = [ { name = "jsonpointer" }, { name = "rfc3339-validator" }, { name = "rfc3986-validator" }, - { name = "rfc3987-syntax" }, { name = "uri-template" }, { name = "webcolors" }, ] @@ -3566,16 +3569,16 @@ wheels = [ [[package]] name = "langchain-openai" -version = "0.3.32" +version = "0.3.35" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "openai" }, { name = "tiktoken" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/19/167d9ad1b6bb75406c4acceda01ef0dc1101c7f629f74441fe8a787fb190/langchain_openai-0.3.32.tar.gz", hash = "sha256:782ad669bd1bdb964456d8882c5178717adcfceecb482cc20005f770e43d346d", size = 782982, upload-time = "2025-08-26T14:25:27.917Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/96/06d0d25a37e05a0ff2d918f0a4b0bf0732aed6a43b472b0b68426ce04ef8/langchain_openai-0.3.35.tar.gz", hash = "sha256:fa985fd041c3809da256a040c98e8a43e91c6d165b96dcfeb770d8bd457bf76f", size = 786635, upload-time = "2025-10-06T15:09:28.463Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/3d/e22ee65fff79afe7bdfbd67844243eb279b440c882dad9e4262dcc87131f/langchain_openai-0.3.32-py3-none-any.whl", hash = "sha256:3354f76822f7cc76d8069831fe2a77f9bc7ff3b4f13af788bd94e4c6e853b400", size = 74531, upload-time = "2025-08-26T14:25:26.542Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d5/c90c5478215c20ee71d8feaf676f7ffd78d0568f8c98bd83f81ce7562ed7/langchain_openai-0.3.35-py3-none-any.whl", hash = "sha256:76d5707e6e81fd461d33964ad618bd326cb661a1975cef7c1cb0703576bdada5", size = 75952, upload-time = "2025-10-06T15:09:27.137Z" }, ] [[package]] @@ -3700,7 +3703,7 @@ wheels = [ [[package]] name = "litellm" -version = "1.80.0" +version = "1.83.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -3716,39 +3719,9 @@ dependencies = [ { name = "tiktoken" }, { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bd/8c/48d533affdbc6d485b7ad4221cd3b40b8c12f9f5568edfe0be0b11e7b945/litellm-1.80.0.tar.gz", hash = "sha256:eeac733eb6b226f9e5fb020f72fe13a32b3354b001dc62bcf1bc4d9b526d6231", size = 11591976, upload-time = "2025-11-16T00:03:51.812Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/53/aa31e4d057b3746b3c323ca993003d6cf15ef987e7fe7ceb53681695ae87/litellm-1.80.0-py3-none-any.whl", hash = "sha256:fd0009758f4772257048d74bf79bb64318859adb4ea49a8b66fdbc718cd80b6e", size = 10492975, upload-time = "2025-11-16T00:03:49.182Z" }, -] - -[[package]] -name = "lupa" -version = "2.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/1c/191c3e6ec6502e3dbe25a53e27f69a5daeac3e56de1f73c0138224171ead/lupa-2.6.tar.gz", hash = "sha256:9a770a6e89576be3447668d7ced312cd6fd41d3c13c2462c9dc2c2ab570e45d9", size = 7240282, upload-time = "2025-10-24T07:20:29.738Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/7c/c095649380adc96c8630273c1768c2ad1e74aa2ee1dd8dd05d218a60569f/litellm-1.83.14.tar.gz", hash = "sha256:24aef9b47cdc424c833e32f3727f411741c690832cd1fe4405e0077144fe09c9", size = 14836599, upload-time = "2026-04-26T03:16:10.176Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/86/ce243390535c39d53ea17ccf0240815e6e457e413e40428a658ea4ee4b8d/lupa-2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47ce718817ef1cc0c40d87c3d5ae56a800d61af00fbc0fad1ca9be12df2f3b56", size = 951707, upload-time = "2025-10-24T07:18:03.884Z" }, - { url = "https://files.pythonhosted.org/packages/86/85/cedea5e6cbeb54396fdcc55f6b741696f3f036d23cfaf986d50d680446da/lupa-2.6-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7aba985b15b101495aa4b07112cdc08baa0c545390d560ad5cfde2e9e34f4d58", size = 1916703, upload-time = "2025-10-24T07:18:05.6Z" }, - { url = "https://files.pythonhosted.org/packages/24/be/3d6b5f9a8588c01a4d88129284c726017b2089f3a3fd3ba8bd977292fea0/lupa-2.6-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:b766f62f95b2739f2248977d29b0722e589dcf4f0ccfa827ccbd29f0148bd2e5", size = 985152, upload-time = "2025-10-24T07:18:08.561Z" }, - { url = "https://files.pythonhosted.org/packages/eb/23/9f9a05beee5d5dce9deca4cb07c91c40a90541fc0a8e09db4ee670da550f/lupa-2.6-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:00a934c23331f94cb51760097ebfab14b005d55a6b30a2b480e3c53dd2fa290d", size = 1159599, upload-time = "2025-10-24T07:18:10.346Z" }, - { url = "https://files.pythonhosted.org/packages/40/4e/e7c0583083db9d7f1fd023800a9767d8e4391e8330d56c2373d890ac971b/lupa-2.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21de9f38bd475303e34a042b7081aabdf50bd9bafd36ce4faea2f90fd9f15c31", size = 1038686, upload-time = "2025-10-24T07:18:12.112Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9f/5a4f7d959d4feba5e203ff0c31889e74d1ca3153122be4a46dca7d92bf7c/lupa-2.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf3bda96d3fc41237e964a69c23647d50d4e28421111360274d4799832c560e9", size = 2071956, upload-time = "2025-10-24T07:18:14.572Z" }, - { url = "https://files.pythonhosted.org/packages/92/34/2f4f13ca65d01169b1720176aedc4af17bc19ee834598c7292db232cb6dc/lupa-2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a76ead245da54801a81053794aa3975f213221f6542d14ec4b859ee2e7e0323", size = 1057199, upload-time = "2025-10-24T07:18:16.379Z" }, - { url = "https://files.pythonhosted.org/packages/35/2a/5f7d2eebec6993b0dcd428e0184ad71afb06a45ba13e717f6501bfed1da3/lupa-2.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8dd0861741caa20886ddbda0a121d8e52fb9b5bb153d82fa9bba796962bf30e8", size = 1173693, upload-time = "2025-10-24T07:18:18.153Z" }, - { url = "https://files.pythonhosted.org/packages/e4/29/089b4d2f8e34417349af3904bb40bec40b65c8731f45e3fd8d497ca573e5/lupa-2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:239e63948b0b23023f81d9a19a395e768ed3da6a299f84e7963b8f813f6e3f9c", size = 2164394, upload-time = "2025-10-24T07:18:20.403Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1b/79c17b23c921f81468a111cad843b076a17ef4b684c4a8dff32a7969c3f0/lupa-2.6-cp312-cp312-win32.whl", hash = "sha256:325894e1099499e7a6f9c351147661a2011887603c71086d36fe0f964d52d1ce", size = 1420647, upload-time = "2025-10-24T07:18:23.368Z" }, - { url = "https://files.pythonhosted.org/packages/b8/15/5121e68aad3584e26e1425a5c9a79cd898f8a152292059e128c206ee817c/lupa-2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c735a1ce8ee60edb0fe71d665f1e6b7c55c6021f1d340eb8c865952c602cd36f", size = 1688529, upload-time = "2025-10-24T07:18:25.523Z" }, - { url = "https://files.pythonhosted.org/packages/28/1d/21176b682ca5469001199d8b95fa1737e29957a3d185186e7a8b55345f2e/lupa-2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:663a6e58a0f60e7d212017d6678639ac8df0119bc13c2145029dcba084391310", size = 947232, upload-time = "2025-10-24T07:18:27.878Z" }, - { url = "https://files.pythonhosted.org/packages/ce/4c/d327befb684660ca13cf79cd1f1d604331808f9f1b6fb6bf57832f8edf80/lupa-2.6-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:d1f5afda5c20b1f3217a80e9bc1b77037f8a6eb11612fd3ada19065303c8f380", size = 1908625, upload-time = "2025-10-24T07:18:29.944Z" }, - { url = "https://files.pythonhosted.org/packages/66/8e/ad22b0a19454dfd08662237a84c792d6d420d36b061f239e084f29d1a4f3/lupa-2.6-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:26f2b3c085fe76e9119e48c1013c1cccdc1f51585d456858290475aa38e7089e", size = 981057, upload-time = "2025-10-24T07:18:31.553Z" }, - { url = "https://files.pythonhosted.org/packages/5c/48/74859073ab276bd0566c719f9ca0108b0cfc1956ca0d68678d117d47d155/lupa-2.6-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:60d2f902c7b96fb8ab98493dcff315e7bb4d0b44dc9dd76eb37de575025d5685", size = 1156227, upload-time = "2025-10-24T07:18:33.981Z" }, - { url = "https://files.pythonhosted.org/packages/09/6c/0e9ded061916877253c2266074060eb71ed99fb21d73c8c114a76725bce2/lupa-2.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a02d25dee3a3250967c36590128d9220ae02f2eda166a24279da0b481519cbff", size = 1035752, upload-time = "2025-10-24T07:18:36.32Z" }, - { url = "https://files.pythonhosted.org/packages/dd/ef/f8c32e454ef9f3fe909f6c7d57a39f950996c37a3deb7b391fec7903dab7/lupa-2.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eae1ee16b886b8914ff292dbefbf2f48abfbdee94b33a88d1d5475e02423203", size = 2069009, upload-time = "2025-10-24T07:18:38.072Z" }, - { url = "https://files.pythonhosted.org/packages/53/dc/15b80c226a5225815a890ee1c11f07968e0aba7a852df41e8ae6fe285063/lupa-2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0edd5073a4ee74ab36f74fe61450148e6044f3952b8d21248581f3c5d1a58be", size = 1056301, upload-time = "2025-10-24T07:18:40.165Z" }, - { url = "https://files.pythonhosted.org/packages/31/14/2086c1425c985acfb30997a67e90c39457122df41324d3c179d6ee2292c6/lupa-2.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0c53ee9f22a8a17e7d4266ad48e86f43771951797042dd51d1494aaa4f5f3f0a", size = 1170673, upload-time = "2025-10-24T07:18:42.426Z" }, - { url = "https://files.pythonhosted.org/packages/10/e5/b216c054cf86576c0191bf9a9f05de6f7e8e07164897d95eea0078dca9b2/lupa-2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:de7c0f157a9064a400d828789191a96da7f4ce889969a588b87ec80de9b14772", size = 2162227, upload-time = "2025-10-24T07:18:46.112Z" }, - { url = "https://files.pythonhosted.org/packages/59/2f/33ecb5bedf4f3bc297ceacb7f016ff951331d352f58e7e791589609ea306/lupa-2.6-cp313-cp313-win32.whl", hash = "sha256:ee9523941ae0a87b5b703417720c5d78f72d2f5bc23883a2ea80a949a3ed9e75", size = 1419558, upload-time = "2025-10-24T07:18:48.371Z" }, - { url = "https://files.pythonhosted.org/packages/f9/b4/55e885834c847ea610e111d87b9ed4768f0afdaeebc00cd46810f25029f6/lupa-2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b1335a5835b0a25ebdbc75cf0bda195e54d133e4d994877ef025e218c2e59db9", size = 1683424, upload-time = "2025-10-24T07:18:50.976Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5c/1b5691575420135e90578543b2bf219497caa33cfd0af64cb38f30288450/litellm-1.83.14-py3-none-any.whl", hash = "sha256:92b11ba2a32cf80707ddf388d18526696c7999a21b418c5e3b6eda1243d2cfdb", size = 16457054, upload-time = "2026-04-26T03:16:05.72Z" }, ] [[package]] @@ -4497,7 +4470,7 @@ wheels = [ [[package]] name = "nltk" -version = "3.9.2" +version = "3.9.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -4505,9 +4478,9 @@ dependencies = [ { name = "regex" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f9/76/3a5e4312c19a028770f86fd7c058cf9f4ec4321c6cf7526bab998a5b683c/nltk-3.9.2.tar.gz", hash = "sha256:0f409e9b069ca4177c1903c3e843eef90c7e92992fa4931ae607da6de49e1419", size = 2887629, upload-time = "2025-10-01T07:19:23.764Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/a1/b3b4adf15585a5bc4c357adde150c01ebeeb642173ded4d871e89468767c/nltk-3.9.4.tar.gz", hash = "sha256:ed03bc098a40481310320808b2db712d95d13ca65b27372f8a403949c8b523d0", size = 2946864, upload-time = "2026-03-24T06:13:40.641Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/90/81ac364ef94209c100e12579629dc92bf7a709a84af32f8c551b02c07e94/nltk-3.9.2-py3-none-any.whl", hash = "sha256:1e209d2b3009110635ed9709a67a1a3e33a10f799490fa71cf4bec218c11c88a", size = 1513404, upload-time = "2025-10-01T07:19:21.648Z" }, + { url = "https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl", hash = "sha256:f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f", size = 1552087, upload-time = "2026-03-24T06:13:38.47Z" }, ] [[package]] @@ -4774,7 +4747,7 @@ wheels = [ [[package]] name = "openai" -version = "1.99.9" +version = "2.24.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -4786,9 +4759,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8a/d2/ef89c6f3f36b13b06e271d3cc984ddd2f62508a0972c1cbcc8485a6644ff/openai-1.99.9.tar.gz", hash = "sha256:f2082d155b1ad22e83247c3de3958eb4255b20ccf4a1de2e6681b6957b554e92", size = 506992, upload-time = "2025-08-12T02:31:10.054Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/13/17e87641b89b74552ed408a92b231283786523edddc95f3545809fab673c/openai-2.24.0.tar.gz", hash = "sha256:1e5769f540dbd01cb33bc4716a23e67b9d695161a734aff9c5f925e2bf99a673", size = 658717, upload-time = "2026-02-24T20:02:07.958Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/fb/df274ca10698ee77b07bff952f302ea627cc12dac6b85289485dd77db6de/openai-1.99.9-py3-none-any.whl", hash = "sha256:9dbcdb425553bae1ac5d947147bebbd630d91bbfc7788394d4c4f3a35682ab3a", size = 786816, upload-time = "2025-08-12T02:31:08.34Z" }, + { url = "https://files.pythonhosted.org/packages/c9/30/844dc675ee6902579b8eef01ed23917cc9319a1c9c0c14ec6e39340c96d0/openai-2.24.0-py3-none-any.whl", hash = "sha256:fed30480d7d6c884303287bde864980a4b137b60553ffbcf9ab4a233b7a73d94", size = 1120122, upload-time = "2026-02-24T20:02:05.669Z" }, ] [[package]] @@ -4959,20 +4932,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/95/f1/b27d3e2e003cd9a3592c43d099d2ed8d0a947c15281bf8463a256db0b46c/opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985", size = 19641, upload-time = "2025-12-11T13:32:22.248Z" }, ] -[[package]] -name = "opentelemetry-exporter-prometheus" -version = "0.60b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-sdk" }, - { name = "prometheus-client" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/14/39/7dafa6fff210737267bed35a8855b6ac7399b9e582b8cf1f25f842517012/opentelemetry_exporter_prometheus-0.60b1.tar.gz", hash = "sha256:a4011b46906323f71724649d301b4dc188aaa068852e814f4df38cc76eac616b", size = 14976, upload-time = "2025-12-11T13:32:42.944Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/0d/4be6bf5477a3eb3d917d2f17d3c0b6720cd6cb97898444a61d43cc983f5c/opentelemetry_exporter_prometheus-0.60b1-py3-none-any.whl", hash = "sha256:49f59178de4f4590e3cef0b8b95cf6e071aae70e1f060566df5546fad773b8fd", size = 13019, upload-time = "2025-12-11T13:32:23.974Z" }, -] - [[package]] name = "opentelemetry-instrumentation" version = "0.60b1" @@ -5206,15 +5165,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, ] -[[package]] -name = "pathvalidate" -version = "3.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/2a/52a8da6fe965dea6192eb716b357558e103aea0a1e9a8352ad575a8406ca/pathvalidate-3.3.1.tar.gz", hash = "sha256:b18c07212bfead624345bb8e1d6141cdcf15a39736994ea0b94035ad2b1ba177", size = 63262, upload-time = "2025-06-15T09:07:20.736Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/70/875f4a23bfc4731703a5835487d0d2fb999031bd415e7d17c0ae615c18b7/pathvalidate-3.3.1-py3-none-any.whl", hash = "sha256:5263baab691f8e1af96092fa5137ee17df5bdfbd6cff1fcac4d6ef4bc2e1735f", size = 24305, upload-time = "2025-06-15T09:07:19.117Z" }, -] - [[package]] name = "pexpect" version = "4.9.0" @@ -5299,6 +5249,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/60/fe31d7e6b8907789dcb0584f88be741ba388413e4fbce35f1eba4e3073de/playwright-1.57.0-py3-none-win_arm64.whl", hash = "sha256:5f065f5a133dbc15e6e7c71e7bc04f258195755b1c32a432b792e28338c8335e", size = 32837940, upload-time = "2025-12-09T08:06:42.268Z" }, ] +[[package]] +name = "playwright-stealth" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "playwright" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/db/6ade5d539c7d151b9defc78fafa8b65aa52352617d0e7699b47008bd801f/playwright_stealth-2.0.3.tar.gz", hash = "sha256:1d8e488fbdd8f190f1269ea8cf5d57d14df3a9f1af1001c41ee3588b2aac3133", size = 25751, upload-time = "2026-04-04T02:50:33.88Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/10/607c409712c02a26c4cb794820514cb7fdaaeac15fb05bed917fb8a354b3/playwright_stealth-2.0.3-py3-none-any.whl", hash = "sha256:1887ade423ab7ff8ae16d363a30a38de0b5817e1e4a29d47b74bf3a0e3dbfcb4", size = 34385, upload-time = "2026-04-04T02:50:35.246Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -5513,21 +5475,21 @@ wheels = [ [[package]] name = "py-key-value-aio" -version = "0.3.0" +version = "0.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beartype" }, - { name = "py-key-value-shared" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/ce/3136b771dddf5ac905cc193b461eb67967cf3979688c6696e1f2cdcde7ea/py_key_value_aio-0.3.0.tar.gz", hash = "sha256:858e852fcf6d696d231266da66042d3355a7f9871650415feef9fca7a6cd4155", size = 50801, upload-time = "2025-11-17T16:50:04.711Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/3c/0397c072a38d4bc580994b42e0c90c5f44f679303489e4376289534735e5/py_key_value_aio-0.4.4.tar.gz", hash = "sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55", size = 92300, upload-time = "2026-02-16T21:21:43.245Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/10/72f6f213b8f0bce36eff21fda0a13271834e9eeff7f9609b01afdc253c79/py_key_value_aio-0.3.0-py3-none-any.whl", hash = "sha256:1c781915766078bfd608daa769fefb97e65d1d73746a3dfb640460e322071b64", size = 96342, upload-time = "2025-11-17T16:50:03.801Z" }, + { url = "https://files.pythonhosted.org/packages/32/69/f1b537ee70b7def42d63124a539ed3026a11a3ffc3086947a1ca6e861868/py_key_value_aio-0.4.4-py3-none-any.whl", hash = "sha256:18e17564ecae61b987f909fc2cd41ee2012c84b4b1dcb8c055cf8b4bc1bf3f5d", size = 152291, upload-time = "2026-02-16T21:21:44.241Z" }, ] [package.optional-dependencies] -disk = [ - { name = "diskcache" }, - { name = "pathvalidate" }, +filetree = [ + { name = "aiofile" }, + { name = "anyio" }, ] keyring = [ { name = "keyring" }, @@ -5535,22 +5497,6 @@ keyring = [ memory = [ { name = "cachetools" }, ] -redis = [ - { name = "redis" }, -] - -[[package]] -name = "py-key-value-shared" -version = "0.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "beartype" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7b/e4/1971dfc4620a3a15b4579fe99e024f5edd6e0967a71154771a059daff4db/py_key_value_shared-0.3.0.tar.gz", hash = "sha256:8fdd786cf96c3e900102945f92aa1473138ebe960ef49da1c833790160c28a4b", size = 11666, upload-time = "2025-11-17T16:50:06.849Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/e4/b8b0a03ece72f47dce2307d36e1c34725b7223d209fc679315ffe6a4e2c3/py_key_value_shared-0.3.0-py3-none-any.whl", hash = "sha256:5b0efba7ebca08bb158b1e93afc2f07d30b8f40c2fc12ce24a4c0d84f42f9298", size = 19560, upload-time = "2025-11-17T16:50:05.954Z" }, -] [[package]] name = "pyarrow" @@ -5754,29 +5700,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, ] -[[package]] -name = "pydocket" -version = "0.16.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cloudpickle" }, - { name = "fakeredis", extra = ["lua"] }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-prometheus" }, - { name = "opentelemetry-instrumentation" }, - { name = "prometheus-client" }, - { name = "py-key-value-aio", extra = ["memory", "redis"] }, - { name = "python-json-logger" }, - { name = "redis" }, - { name = "rich" }, - { name = "typer" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4d/c6/eb7f3af72fa5c04b52a3f9390ff0c948987441987f9526dd992d2a6b3524/pydocket-0.16.4.tar.gz", hash = "sha256:d034d1ac75877560d86329fb3643e7b862fcbcdac407d876a62f5d9e386e8753", size = 297949, upload-time = "2026-01-08T21:58:31.637Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/74/c5/e6ffed3902ead6cb906758749c42211f7f24ea6d8fdd772f531f5a81c9fa/pydocket-0.16.4-py3-none-any.whl", hash = "sha256:cdcdf74b987c2cd5d03c7353d15f8dd2ac9bd43f2a91f7441748d5a8ebd617c9", size = 67374, upload-time = "2026-01-08T21:58:30.01Z" }, -] - [[package]] name = "pydub" version = "0.25.1" @@ -8581,11 +8504,11 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.2.1" +version = "1.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] [[package]] @@ -8893,15 +8816,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/21/f691fb2613100a62b3fa91e9988c991e9ca5b89ea31c0d3152a3210344f9/rank_bm25-0.2.2-py3-none-any.whl", hash = "sha256:7bd4a95571adadfc271746fa146a4bcfd89c0cf731e49c3d1ad863290adbe8ae", size = 8584, upload-time = "2022-02-16T12:10:50.626Z" }, ] -[[package]] -name = "redis" -version = "7.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" }, -] - [[package]] name = "referencing" version = "0.36.2" @@ -9014,18 +8928,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242, upload-time = "2019-10-28T16:00:13.976Z" }, ] -[[package]] -name = "rfc3987-syntax" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "lark" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2c/06/37c1a5557acf449e8e406a830a05bf885ac47d33270aec454ef78675008d/rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d", size = 14239, upload-time = "2025-07-18T01:05:05.015Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" }, -] - [[package]] name = "rich" version = "14.2.0" @@ -9540,15 +9442,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a", size = 93002, upload-time = "2021-11-16T18:38:34.792Z" }, ] -[[package]] -name = "sortedcontainers" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, -] - [[package]] name = "soundfile" version = "0.13.1" @@ -9742,19 +9635,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/fc/5e2988590ff2e0128eea6446806c904445a44e17256c67141573ea16b5a5/textual-6.11.0-py3-none-any.whl", hash = "sha256:9e663b73ed37123a9b13c16a0c85e09ef917a4cfded97814361ed5cccfa40f89", size = 714886, upload-time = "2025-12-18T10:48:36.269Z" }, ] -[[package]] -name = "tf-playwright-stealth" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "fake-http-header" }, - { name = "playwright" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d6/6b/32bb58c65991f91aeaaf7473b650175d9d4af5dd383983d177d49ccba08d/tf_playwright_stealth-1.2.0.tar.gz", hash = "sha256:7bb8d32d3e60324fbf6b9eeae540b8cd9f3b9e07baeb33b025dbc98ad47658ba", size = 23362, upload-time = "2025-06-13T04:51:04.97Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/3d/2653f4cf49660bb44eeac8270617cc4c0287d61716f249f55053f0af0724/tf_playwright_stealth-1.2.0-py3-none-any.whl", hash = "sha256:26ee47ee89fa0f43c606fe37c188ea3ccd36f96ea90c01d167b768df457e7886", size = 33151, upload-time = "2025-06-13T04:51:03.769Z" }, -] - [[package]] name = "threadpoolctl" version = "3.6.0" @@ -9839,27 +9719,28 @@ wheels = [ [[package]] name = "tokenizers" -version = "0.21.4" +version = "0.22.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/2f/402986d0823f8d7ca139d969af2917fefaa9b947d1fb32f6168c509f2492/tokenizers-0.21.4.tar.gz", hash = "sha256:fa23f85fbc9a02ec5c6978da172cdcbac23498c3ca9f3645c5c68740ac007880", size = 351253, upload-time = "2025-07-28T15:48:54.325Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/c6/fdb6f72bf6454f52eb4a2510be7fb0f614e541a2554d6210e370d85efff4/tokenizers-0.21.4-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:2ccc10a7c3bcefe0f242867dc914fc1226ee44321eb618cfe3019b5df3400133", size = 2863987, upload-time = "2025-07-28T15:48:44.877Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a6/28975479e35ddc751dc1ddc97b9b69bf7fcf074db31548aab37f8116674c/tokenizers-0.21.4-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:5e2f601a8e0cd5be5cc7506b20a79112370b9b3e9cb5f13f68ab11acd6ca7d60", size = 2732457, upload-time = "2025-07-28T15:48:43.265Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8f/24f39d7b5c726b7b0be95dca04f344df278a3fe3a4deb15a975d194cbb32/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b376f5a1aee67b4d29032ee85511bbd1b99007ec735f7f35c8a2eb104eade5", size = 3012624, upload-time = "2025-07-28T13:22:43.895Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/26358925717687a58cb74d7a508de96649544fad5778f0cd9827398dc499/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2107ad649e2cda4488d41dfd031469e9da3fcbfd6183e74e4958fa729ffbf9c6", size = 2939681, upload-time = "2025-07-28T13:22:47.499Z" }, - { url = "https://files.pythonhosted.org/packages/99/6f/cc300fea5db2ab5ddc2c8aea5757a27b89c84469899710c3aeddc1d39801/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c73012da95afafdf235ba80047699df4384fdc481527448a078ffd00e45a7d9", size = 3247445, upload-time = "2025-07-28T15:48:39.711Z" }, - { url = "https://files.pythonhosted.org/packages/be/bf/98cb4b9c3c4afd8be89cfa6423704337dc20b73eb4180397a6e0d456c334/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f23186c40395fc390d27f519679a58023f368a0aad234af145e0f39ad1212732", size = 3428014, upload-time = "2025-07-28T13:22:49.569Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/96c1cc780e6ca7f01a57c13235dd05b7bc1c0f3588512ebe9d1331b5f5ae/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc88bb34e23a54cc42713d6d98af5f1bf79c07653d24fe984d2d695ba2c922a2", size = 3193197, upload-time = "2025-07-28T13:22:51.471Z" }, - { url = "https://files.pythonhosted.org/packages/f2/90/273b6c7ec78af547694eddeea9e05de771278bd20476525ab930cecaf7d8/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51b7eabb104f46c1c50b486520555715457ae833d5aee9ff6ae853d1130506ff", size = 3115426, upload-time = "2025-07-28T15:48:41.439Z" }, - { url = "https://files.pythonhosted.org/packages/91/43/c640d5a07e95f1cf9d2c92501f20a25f179ac53a4f71e1489a3dcfcc67ee/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:714b05b2e1af1288bd1bc56ce496c4cebb64a20d158ee802887757791191e6e2", size = 9089127, upload-time = "2025-07-28T15:48:46.472Z" }, - { url = "https://files.pythonhosted.org/packages/44/a1/dd23edd6271d4dca788e5200a807b49ec3e6987815cd9d0a07ad9c96c7c2/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1340ff877ceedfa937544b7d79f5b7becf33a4cfb58f89b3b49927004ef66f78", size = 9055243, upload-time = "2025-07-28T15:48:48.539Z" }, - { url = "https://files.pythonhosted.org/packages/21/2b/b410d6e9021c4b7ddb57248304dc817c4d4970b73b6ee343674914701197/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3c1f4317576e465ac9ef0d165b247825a2a4078bcd01cba6b54b867bdf9fdd8b", size = 9298237, upload-time = "2025-07-28T15:48:50.443Z" }, - { url = "https://files.pythonhosted.org/packages/b7/0a/42348c995c67e2e6e5c89ffb9cfd68507cbaeb84ff39c49ee6e0a6dd0fd2/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c212aa4e45ec0bb5274b16b6f31dd3f1c41944025c2358faaa5782c754e84c24", size = 9461980, upload-time = "2025-07-28T15:48:52.325Z" }, - { url = "https://files.pythonhosted.org/packages/3d/d3/dacccd834404cd71b5c334882f3ba40331ad2120e69ded32cf5fda9a7436/tokenizers-0.21.4-cp39-abi3-win32.whl", hash = "sha256:6c42a930bc5f4c47f4ea775c91de47d27910881902b0f20e4990ebe045a415d0", size = 2329871, upload-time = "2025-07-28T15:48:56.841Z" }, - { url = "https://files.pythonhosted.org/packages/41/f2/fd673d979185f5dcbac4be7d09461cbb99751554ffb6718d0013af8604cb/tokenizers-0.21.4-cp39-abi3-win_amd64.whl", hash = "sha256:475d807a5c3eb72c59ad9b5fcdb254f6e17f53dfcbb9903233b0dfa9c943b597", size = 2507568, upload-time = "2025-07-28T15:48:55.456Z" }, + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, ] [[package]] @@ -9990,28 +9871,7 @@ wheels = [ [[package]] name = "transformers" -version = "4.55.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "filelock" }, - { name = "huggingface-hub" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "pyyaml" }, - { name = "regex" }, - { name = "requests" }, - { name = "safetensors" }, - { name = "tokenizers" }, - { name = "tqdm" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2b/43/3cb831d5f28cc723516e5bb43a8c6042aca3038bb36b6bd6016b40dfd1e8/transformers-4.55.4.tar.gz", hash = "sha256:574a30559bc273c7a4585599ff28ab6b676e96dc56ffd2025ecfce2fd0ab915d", size = 9573015, upload-time = "2025-08-22T15:18:43.192Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/0a/8791a6ee0529c45f669566969e99b75e2ab20eb0bfee8794ce295c18bdad/transformers-4.55.4-py3-none-any.whl", hash = "sha256:df28f3849665faba4af5106f0db4510323277c4bb595055340544f7e59d06458", size = 11269659, upload-time = "2025-08-22T15:18:40.025Z" }, -] - -[[package]] -name = "transformers-v4-55-0-glm-4-5v-preview" -version = "4.56.0.dev0" +version = "4.57.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -10025,9 +9885,9 @@ dependencies = [ { name = "tokenizers" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/b5/1f1d81da95cd246768b7443aa9f36fac31434a82afe6e8a4a37a4e69f8b4/transformers_v4_55_0_glm_4_5v_preview-4.56.0.dev0.tar.gz", hash = "sha256:ba524343d4cf715f3b17df3e3e0f3c99f5c19d8261d53a738b65374dedbf04f3", size = 10353786, upload-time = "2025-08-11T10:54:06.598Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/35/67252acc1b929dc88b6602e8c4a982e64f31e733b804c14bc24b47da35e6/transformers-4.57.6.tar.gz", hash = "sha256:55e44126ece9dc0a291521b7e5492b572e6ef2766338a610b9ab5afbb70689d3", size = 10134912, upload-time = "2026-01-16T10:38:39.284Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/f9/d69d7fdaaad3f952060417d82114b9168bf0da1058fad3a69bcefd3e29df/transformers_v4_55_0_glm_4_5v_preview-4.56.0.dev0-py3-none-any.whl", hash = "sha256:53d656051a7aef44933544d56d70551f5ce8310b99ee6feedf2f87213f11d1b8", size = 12261492, upload-time = "2025-08-11T10:54:02.778Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/e484ef633af3887baeeb4b6ad12743363af7cce68ae51e938e00aaa0529d/transformers-4.57.6-py3-none-any.whl", hash = "sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550", size = 11993498, upload-time = "2026-01-16T10:38:31.289Z" }, ] [[package]] @@ -10190,6 +10050,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/c7/fb42228bb05473d248c110218ffb8b1ad2f76728ed8699856e5af21112ad/ultralytics_thop-2.0.18-py3-none-any.whl", hash = "sha256:2bb44851ad224b116c3995b02dd5e474a5ccf00acf237fe0edb9e1506ede04ec", size = 28941, upload-time = "2025-10-29T16:58:12.093Z" }, ] +[[package]] +name = "uncalled-for" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/82/345cc927f7fbdae6065e7768759932fcc827fc20b29b45dfbafa2f1f7da4/uncalled_for-0.3.2.tar.gz", hash = "sha256:89f5dbcd71e2b8f47c030b1fa302e6cce2ec795d1ac565eeb6525c5fe55cb8a2", size = 50032, upload-time = "2026-05-06T13:38:25.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/25/2c87754f3a9e692315f7b811244090e68f362979fc8886b3fbd2985a1d8c/uncalled_for-0.3.2-py3-none-any.whl", hash = "sha256:0ff60b142c7d1f8070bde9d42afaa70aedc77dcc10998c227687e9c15713418e", size = 11444, upload-time = "2026-05-06T13:38:24.025Z" }, +] + +[[package]] +name = "unclecode-litellm" +version = "1.81.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "click" }, + { name = "fastuuid" }, + { name = "httpx" }, + { name = "importlib-metadata" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "tiktoken" }, + { name = "tokenizers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/c4/93ed52c49c2347184f908c692ebb7c1f06303805910774c3282ac68033db/unclecode_litellm-1.81.13.tar.gz", hash = "sha256:db70e34e3e859c0a07f02cb02eaa644f8fa4b4ecc5e2f3be9a58bd7d1c3feedc", size = 16678208, upload-time = "2026-03-24T14:46:31.915Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/85/7b1e0bc5827bcb23dc572b17c447fb5825340f36a9e4405d4b777b862e0c/unclecode_litellm-1.81.13-py3-none-any.whl", hash = "sha256:5e1fbedbed92333b48e7371e0bacf86d1288020451bf34351703c3b159591399", size = 18008619, upload-time = "2026-03-24T14:46:28.009Z" }, +] + [[package]] name = "universal-pathlib" version = "0.3.7" From 71048c42769ed398aca590653f9694934c3da4b8 Mon Sep 17 00:00:00 2001 From: zeroxjf Date: Mon, 18 May 2026 12:01:00 -0400 Subject: [PATCH 2/5] Fix Python formatting for lint --- docs/scripts/modal_app.py | 4 +- examples/sandboxes/test_linux_cloud_vm.py | 4 +- .../tests/integration/driver_client.py | 7 +- .../tests/integration/test_api_parity.py | 85 +++-- .../integration/test_background_focus.py | 272 ++++++++------ .../tests/integration/test_cli.py | 17 +- .../integration/test_concurrent_drivers.py | 129 ++++--- .../integration/test_cursor_visibility.py | 134 ++++--- .../integration/test_double_click_delivery.py | 76 ++-- .../integration/test_focus_steal_parity.py | 4 +- .../tests/integration/test_list_windows.py | 25 +- .../tests/integration/test_overlay_z_order.py | 28 +- .../integration/test_pixel_click_delivery.py | 90 +++-- .../tests/integration/v2/conftest.py | 28 +- .../tests/integration/v2/harness/cv.py | 8 +- .../tests/integration/v2/harness/driver.py | 37 +- .../tests/integration/v2/harness/monitor.py | 20 +- .../tests/integration/v2/harness/tree.py | 2 +- .../tests/integration/v2/test_blender.py | 17 +- .../tests/integration/v2/test_chrome.py | 35 +- .../tests/integration/v2/test_electron.py | 31 +- .../tests/integration/v2/test_safari.py | 82 +++-- .../tests/integration/v2/test_tauri.py | 19 +- libs/cua-driver/Tests/integration/conftest.py | 34 +- .../Tests/integration/driver_client.py | 7 +- .../Tests/integration/harness/cv.py | 8 +- .../Tests/integration/harness/driver.py | 44 ++- .../Tests/integration/harness/monitor.py | 20 +- .../Tests/integration/harness/tree.py | 2 +- .../integration/test_background_focus.py | 345 +++++++++++------- .../test_background_menu_shortcut.py | 52 ++- .../Tests/integration/test_blender.py | 17 +- .../Tests/integration/test_browser_js.py | 192 ++++++---- .../integration/test_check_permissions_cli.py | 17 +- .../Tests/integration/test_chrome.py | 36 +- .../integration/test_chrome_minimized_nav.py | 183 ++++++---- .../test_click_opens_new_window.py | 52 +-- .../Tests/integration/test_click_pixel_ax.py | 12 +- .../integration/test_drag_slider_delivery.py | 60 ++- .../Tests/integration/test_electron.py | 31 +- .../test_hermes_chrome_form_fill.py | 166 +++++---- .../integration/test_hermes_form_fill.py | 84 +++-- .../integration/test_hermes_form_fill_qwen.py | 66 ++-- .../test_hermes_launch_safari_bg.py | 62 ++-- .../integration/test_hidden_app_capture.py | 27 +- .../integration/test_launch_app_visible.py | 8 +- .../Tests/integration/test_list_windows.py | 31 +- .../Tests/integration/test_overlay_z_order.py | 17 +- .../Tests/integration/test_safari.py | 82 +++-- .../Tests/integration/test_tauri.py | 19 +- .../Tests/integration/test_webkit_js.py | 80 ++-- .../computer_server/handlers/macos.py | 3 +- .../computer-server/computer_server/main.py | 4 +- .../tests/test_auth_availability.py | 8 +- .../tests/test_window_launch.py | 1 - .../cua-auto/tests/test_window_launch.py | 1 - .../cua_sandbox/transport/cloud.py | 6 +- .../cua-sandbox/cua_sandbox/transport/http.py | 8 +- .../tests/test_transport_http_retry.py | 14 +- 59 files changed, 1742 insertions(+), 1211 deletions(-) diff --git a/docs/scripts/modal_app.py b/docs/scripts/modal_app.py index cb317bfccb..ad1d308a0a 100644 --- a/docs/scripts/modal_app.py +++ b/docs/scripts/modal_app.py @@ -390,7 +390,7 @@ def sync_to_s3(bucket: str = S3_BUCKET_NAME): key = f"docs_db/docs.lance/{fpath.relative_to(lance_dir)}" s3.upload_file(str(fpath), bucket, key) uploaded += 1 - print(f" Uploaded docs LanceDB directory") + print(" Uploaded docs LanceDB directory") # --- code databases --- code_db_dir = Path(CODE_DB_PATH) @@ -411,7 +411,7 @@ def sync_to_s3(bucket: str = S3_BUCKET_NAME): key = f"code_db/code_index.lancedb/{fpath.relative_to(code_lance)}" s3.upload_file(str(fpath), bucket, key) uploaded += 1 - print(f" Uploaded code LanceDB directory") + print(" Uploaded code LanceDB directory") print(f"S3 sync complete: {uploaded} files uploaded to s3://{bucket}/") return {"bucket": bucket, "files_uploaded": uploaded} diff --git a/examples/sandboxes/test_linux_cloud_vm.py b/examples/sandboxes/test_linux_cloud_vm.py index 286a9a28ed..6c398ee9a4 100644 --- a/examples/sandboxes/test_linux_cloud_vm.py +++ b/examples/sandboxes/test_linux_cloud_vm.py @@ -75,7 +75,9 @@ async def test_linux_cloud_vm(): len(screenshot), time.monotonic() - t2, ) - assert screenshot[:4] == b"\x89PNG", f"Screenshot not PNG: first 4 bytes = {screenshot[:4]!r}" + assert ( + screenshot[:4] == b"\x89PNG" + ), f"Screenshot not PNG: first 4 bytes = {screenshot[:4]!r}" total_time = time.monotonic() - t0 logger.info("Test passed in %.1fs (provision=%.1fs)", total_time, provision_time) diff --git a/libs/cua-driver-rs/tests/integration/driver_client.py b/libs/cua-driver-rs/tests/integration/driver_client.py index 850b16abeb..365404a072 100644 --- a/libs/cua-driver-rs/tests/integration/driver_client.py +++ b/libs/cua-driver-rs/tests/integration/driver_client.py @@ -94,9 +94,7 @@ def _notify(self, method: str, params: Optional[dict] = None) -> None: payload["params"] = params self._write(payload) - def _call( - self, method: str, params: Optional[dict] = None, timeout: float = 20.0 - ) -> dict: + def _call(self, method: str, params: Optional[dict] = None, timeout: float = 20.0) -> dict: self._next_id += 1 request_id = self._next_id payload = {"jsonrpc": "2.0", "id": request_id, "method": method} @@ -198,8 +196,7 @@ def resolve_window_id( if require_on_current_space: preferred = [ - w for w in windows - if w.get("is_on_screen") and w.get("on_current_space") is not False + w for w in windows if w.get("is_on_screen") and w.get("on_current_space") is not False ] if preferred: preferred.sort(key=lambda w: w.get("z_index", 0), reverse=True) diff --git a/libs/cua-driver-rs/tests/integration/test_api_parity.py b/libs/cua-driver-rs/tests/integration/test_api_parity.py index 3fbe1c2c50..6276980e10 100644 --- a/libs/cua-driver-rs/tests/integration/test_api_parity.py +++ b/libs/cua-driver-rs/tests/integration/test_api_parity.py @@ -47,6 +47,7 @@ # ── helpers ──────────────────────────────────────────────────────────────────── + def _run( args: list[str], timeout: int = 20, @@ -129,9 +130,9 @@ def _swift_binary() -> str | None: # In Rust, not in Swift yet. Tests for these will fail on the Swift binary. RUST_ONLY_TOOLS = [ - "type_text_chars", # per-character delay typing; Swift uses type_text - "browser_eval", # CDP JS eval; Swift uses 'page' for browser JS - "get_accessibility_tree",# lightweight desktop AX snapshot (separate from get_window_state) + "type_text_chars", # per-character delay typing; Swift uses type_text + "browser_eval", # CDP JS eval; Swift uses 'page' for browser JS + "get_accessibility_tree", # lightweight desktop AX snapshot (separate from get_window_state) ] # In Swift, not registered in Rust yet (page.rs exists but not in register_all). @@ -142,6 +143,7 @@ def _swift_binary() -> str | None: # ── parity mixin ─────────────────────────────────────────────────────────────── + class _ParityMixin: """All API surface tests. Concrete subclasses set `cls.binary`.""" @@ -185,8 +187,14 @@ def test_list_tools_contains_required_tool_names(self) -> None: r = _run([self.binary, "list-tools"]) self.assertEqual(r.returncode, 0) for name in [ - "click", "screenshot", "list_windows", "press_key", - "get_window_state", "type_text", "hotkey", "scroll", + "click", + "screenshot", + "list_windows", + "press_key", + "get_window_state", + "type_text", + "hotkey", + "scroll", ]: self.assertIn(name, r.stdout, f"list-tools missing: {name!r}") @@ -216,7 +224,8 @@ def test_describe_each_required_tool_exits_zero(self) -> None: with self.subTest(tool=name): r = _run([self.binary, "describe", name]) self.assertEqual( - r.returncode, 0, + r.returncode, + 0, f"describe {name!r} exited {r.returncode}: {r.stderr}", ) self.assertIn(name, r.stdout) @@ -331,10 +340,7 @@ def test_call_check_permissions_returns_accessibility_and_screen_recording( try: data = json.loads(r.stdout) except json.JSONDecodeError: - self.fail( - f"call check_permissions did not output JSON.\n" - f"stdout: {r.stdout!r}" - ) + self.fail(f"call check_permissions did not output JSON.\n" f"stdout: {r.stdout!r}") self.assertIn("accessibility", data) self.assertIn("screen_recording", data) @@ -351,8 +357,11 @@ def test_call_get_config_returns_config_object(self) -> None: data = json.loads(r.stdout) # Config must have at least one of the known top-level keys. known_keys = { - "schema_version", "capture_mode", "max_image_dimension", - "agent_cursor", "telemetry_enabled", + "schema_version", + "capture_mode", + "max_image_dimension", + "agent_cursor", + "telemetry_enabled", } self.assertTrue( known_keys.intersection(data.keys()), @@ -407,8 +416,12 @@ def test_call_get_window_state_for_calculator(self) -> None: # get_window_state rs = _run( - [self.binary, "call", "get_window_state", - f'{{"pid": {pid}, "window_id": {window_id}}}'], + [ + self.binary, + "call", + "get_window_state", + f'{{"pid": {pid}, "window_id": {window_id}}}', + ], timeout=30, ) self.assertEqual(rs.returncode, 0, f"get_window_state stderr: {rs.stderr}") @@ -427,7 +440,7 @@ def test_call_press_key_missing_pid_exits_1(self) -> None: self.assertEqual(r.returncode, 1, f"stdout: {r.stdout}") def test_call_click_missing_required_args_exits_1(self) -> None: - r = _run([self.binary, "call", "click", '{}']) + r = _run([self.binary, "call", "click", "{}"]) self.assertEqual(r.returncode, 1, f"stdout: {r.stdout}") def test_call_type_text_missing_pid_exits_1(self) -> None: @@ -439,11 +452,11 @@ def test_call_hotkey_missing_pid_exits_1(self) -> None: self.assertEqual(r.returncode, 1, f"stdout: {r.stdout}") def test_call_scroll_missing_required_args_exits_1(self) -> None: - r = _run([self.binary, "call", "scroll", '{}']) + r = _run([self.binary, "call", "scroll", "{}"]) self.assertEqual(r.returncode, 1, f"stdout: {r.stdout}") def test_call_drag_missing_required_args_exits_1(self) -> None: - r = _run([self.binary, "call", "drag", '{}']) + r = _run([self.binary, "call", "drag", "{}"]) self.assertEqual(r.returncode, 1, f"stdout: {r.stdout}") def test_call_get_window_state_missing_pid_exits_1(self) -> None: @@ -451,7 +464,7 @@ def test_call_get_window_state_missing_pid_exits_1(self) -> None: self.assertEqual(r.returncode, 1, f"stdout: {r.stdout}") def test_call_launch_app_no_args_exits_1(self) -> None: - r = _run([self.binary, "call", "launch_app", '{}']) + r = _run([self.binary, "call", "launch_app", "{}"]) self.assertEqual(r.returncode, 1, f"stdout: {r.stdout}") # ── CLI: implicit call (tool name as first positional) ──────────────────── @@ -853,10 +866,16 @@ def test_mcp_get_config_roundtrip(self) -> None: with self._mcp() as c: result = c.call_tool("get_config") sc = result.get("structuredContent", result) - known = {"schema_version", "capture_mode", "max_image_dimension", - "agent_cursor", "telemetry_enabled"} - self.assertTrue(known.intersection(sc.keys()), - f"get_config has no known keys: {list(sc.keys())}") + known = { + "schema_version", + "capture_mode", + "max_image_dimension", + "agent_cursor", + "telemetry_enabled", + } + self.assertTrue( + known.intersection(sc.keys()), f"get_config has no known keys: {list(sc.keys())}" + ) def test_mcp_set_config_max_image_dimension(self) -> None: with self._mcp() as c: @@ -881,6 +900,7 @@ def test_mcp_get_recording_state(self) -> None: def test_mcp_set_recording_enable_disable(self) -> None: import tempfile + with tempfile.TemporaryDirectory() as tmpdir: with self._mcp() as c: # Enable recording — output_dir is required. @@ -952,9 +972,7 @@ def test_mcp_launch_app_missing_args_raises_error(self) -> None: self._assert_tool_raises_mcp_error("launch_app", {}) def test_mcp_set_value_missing_pid_raises_error(self) -> None: - self._assert_tool_raises_mcp_error( - "set_value", {"element_index": 0, "value": "x"} - ) + self._assert_tool_raises_mcp_error("set_value", {"element_index": 0, "value": "x"}) def test_mcp_replay_trajectory_bad_dir_raises_error(self) -> None: self._assert_tool_raises_mcp_error( @@ -1000,9 +1018,7 @@ def test_mcp_get_window_state_has_required_sc_keys(self) -> None: wins = c.call_tool("list_windows", {"pid": pid})["structuredContent"]["windows"] if not wins: self.skipTest("Calculator has no windows") - state = c.call_tool("get_window_state", { - "pid": pid, "window_id": wins[0]["window_id"] - }) + state = c.call_tool("get_window_state", {"pid": pid, "window_id": wins[0]["window_id"]}) sc = state.get("structuredContent", state) for key in ("tree_markdown", "screenshot_width", "screenshot_height"): self.assertIn(key, sc, f"get_window_state structuredContent missing: {key!r}") @@ -1045,9 +1061,7 @@ def test_mcp_set_agent_cursor_motion_arc(self) -> None: # ── stdio MCP: zoom ─────────────────────────────────────────────────────── def test_mcp_zoom_missing_pid_raises_error(self) -> None: - self._assert_tool_raises_mcp_error( - "zoom", {"x1": 0, "y1": 0, "x2": 100, "y2": 100} - ) + self._assert_tool_raises_mcp_error("zoom", {"x1": 0, "y1": 0, "x2": 100, "y2": 100}) # ── stdio MCP: page ─────────────────────────────────────────────────────── @@ -1063,9 +1077,7 @@ def test_mcp_page_is_registered(self) -> None: self.assertIn("page", names, "'page' tool is not registered") def test_mcp_page_missing_pid_raises_error(self) -> None: - self._assert_tool_raises_mcp_error( - "page", {"action": "get_text"} - ) + self._assert_tool_raises_mcp_error("page", {"action": "get_text"}) # ── stdio MCP: Rust-only tools (missing in Swift) ──────────────────────── @@ -1133,9 +1145,7 @@ def test_mcp_browser_eval_is_registered(self) -> None: def test_mcp_browser_eval_missing_pid_raises_error(self) -> None: """browser_eval with no pid should return an MCP error.""" - self._assert_tool_raises_mcp_error( - "browser_eval", {"expression": "1+1"} - ) + self._assert_tool_raises_mcp_error("browser_eval", {"expression": "1+1"}) # ── stdio MCP: protocol-level contracts ────────────────────────────────── @@ -1173,6 +1183,7 @@ def test_mcp_protocol_version_echo(self) -> None: # ── concrete test classes (one per binary) ──────────────────────────────────── + class SwiftParityTests(_ParityMixin, unittest.TestCase): """Run the full parity suite against the Swift cua-driver binary.""" diff --git a/libs/cua-driver-rs/tests/integration/test_background_focus.py b/libs/cua-driver-rs/tests/integration/test_background_focus.py index b86e1fbe9a..c1aa23931f 100644 --- a/libs/cua-driver-rs/tests/integration/test_background_focus.py +++ b/libs/cua-driver-rs/tests/integration/test_background_focus.py @@ -45,9 +45,7 @@ # FocusMonitorApp lives in the Swift cua-driver tree (shared fixture). _FOCUS_APP_DIR = os.path.join(_LIBS_ROOT, "cua-driver", "Tests", "FocusMonitorApp") _FOCUS_APP_BUNDLE = os.path.join(_FOCUS_APP_DIR, "FocusMonitorApp.app") -_FOCUS_APP_EXE = os.path.join( - _FOCUS_APP_BUNDLE, "Contents", "MacOS", "FocusMonitorApp" -) +_FOCUS_APP_EXE = os.path.join(_FOCUS_APP_BUNDLE, "Contents", "MacOS", "FocusMonitorApp") _LOSS_FILE = "/tmp/focus_monitor_losses.txt" SAFARI_BUNDLE = "com.apple.Safari" @@ -58,11 +56,10 @@ # Helpers # --------------------------------------------------------------------------- + def _build_focus_app() -> None: if not os.path.exists(_FOCUS_APP_EXE): - subprocess.run( - [os.path.join(_FOCUS_APP_DIR, "build.sh")], check=True - ) + subprocess.run([os.path.join(_FOCUS_APP_DIR, "build.sh")], check=True) def _launch_focus_app() -> tuple[subprocess.Popen, int]: @@ -118,9 +115,7 @@ def _open_safari_to_html(client: DriverClient) -> int: def _get_page_text(client: DriverClient, pid: int) -> str: """Return the AX tree markdown for Safari.""" window_id = resolve_window_id(client, pid) - result = client.call_tool( - "get_window_state", {"pid": pid, "window_id": window_id} - ) + result = client.call_tool("get_window_state", {"pid": pid, "window_id": window_id}) return result.get("structuredContent", result).get("tree_markdown", "") @@ -128,7 +123,7 @@ def _find_element_index(tree_markdown: str, label: str) -> int | None: """Extract the first [N] element index from a line containing `label`.""" for line in tree_markdown.split("\n"): if label in line: - m = re.search(r'\[(\d+)\]', line) + m = re.search(r"\[(\d+)\]", line) if m: return int(m.group(1)) return None @@ -139,15 +134,15 @@ def _find_calc_button(tree_markdown: str, label: str) -> int | None: for line in tree_markdown.split("\n"): if "AXButton" not in line: continue - m = re.search(r'\[(\d+)\]', line) + m = re.search(r"\[(\d+)\]", line) if not m: continue idx = int(m.group(1)) - if f'({label})' in line: + if f"({label})" in line: return idx if f'help="{label}' in line: return idx - if f'id={label}' in line: + if f"id={label}" in line: return idx return None @@ -165,6 +160,7 @@ def _activate_focus_monitor() -> None: # Safari background focus tests # --------------------------------------------------------------------------- + class BackgroundFocusTests(unittest.TestCase): """Click & type into backgrounded Safari without stealing focus.""" @@ -200,9 +196,9 @@ def setUpClass(cls) -> None: # Confirm FocusMonitorApp is frontmost. with DriverClient(cls.binary) as c: active = frontmost_bundle_id(c) - assert active == FOCUS_MONITOR_BUNDLE, ( - f"Expected FocusMonitorApp frontmost, got {active}" - ) + assert ( + active == FOCUS_MONITOR_BUNDLE + ), f"Expected FocusMonitorApp frontmost, got {active}" # Baseline: 0 focus losses. losses = _read_focus_losses() @@ -210,7 +206,7 @@ def setUpClass(cls) -> None: @classmethod def tearDownClass(cls) -> None: - if hasattr(cls, '_focus_proc'): + if hasattr(cls, "_focus_proc"): cls._focus_proc.terminate() try: cls._focus_proc.wait(timeout=3) @@ -232,7 +228,8 @@ def setUp(self) -> None: with DriverClient(self.binary) as c: active = frontmost_bundle_id(c) self.assertEqual( - active, FOCUS_MONITOR_BUNDLE, + active, + FOCUS_MONITOR_BUNDLE, f"FocusMonitorApp not frontmost at test start, got {active}", ) @@ -242,37 +239,45 @@ def _assert_no_focus_loss(self, label: str) -> None: losses = _read_focus_losses() with DriverClient(self.binary) as c: active = frontmost_bundle_id(c) - focus_restored = (active == FOCUS_MONITOR_BUNDLE) + focus_restored = active == FOCUS_MONITOR_BUNDLE loss_delta = losses - self._losses_before - print(f" [{label}] losses: {self._losses_before}->{losses} " - f"(delta={loss_delta}), frontmost: {active}, " - f"restored: {focus_restored}") + print( + f" [{label}] losses: {self._losses_before}->{losses} " + f"(delta={loss_delta}), frontmost: {active}, " + f"restored: {focus_restored}" + ) self.assertEqual( - active, FOCUS_MONITOR_BUNDLE, - f"[{label}] Focus not restored — " - f"frontmost is {active}, not FocusMonitorApp", + active, + FOCUS_MONITOR_BUNDLE, + f"[{label}] Focus not restored — " f"frontmost is {active}, not FocusMonitorApp", ) def test_01_ax_click_button(self) -> None: """AX-click the 'Click Me' button in backgrounded Safari.""" with DriverClient(self.binary) as c: window_id = resolve_window_id(c, self._safari_pid) - snap = c.call_tool("get_window_state", { - "pid": self._safari_pid, - "window_id": window_id, - "query": "Click Me", - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._safari_pid, + "window_id": window_id, + "query": "Click Me", + }, + ) tree = snap.get("structuredContent", snap).get("tree_markdown", "") print(f"\n filtered tree:\n{tree}") idx = _find_element_index(tree, "Click Me") self.assertIsNotNone(idx, "Could not find 'Click Me' button in AX tree") - result = c.call_tool("click", { - "pid": self._safari_pid, - "window_id": window_id, - "element_index": idx, - }) + result = c.call_tool( + "click", + { + "pid": self._safari_pid, + "window_id": window_id, + "element_index": idx, + }, + ) print(f" click result: {result}") time.sleep(0.5) @@ -287,11 +292,14 @@ def test_02_type_text_chars(self) -> None: """Type into the text field in backgrounded Safari via keystroke synthesis.""" with DriverClient(self.binary) as c: window_id = resolve_window_id(c, self._safari_pid) - snap = c.call_tool("get_window_state", { - "pid": self._safari_pid, - "window_id": window_id, - "query": "AXTextField", - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._safari_pid, + "window_id": window_id, + "query": "AXTextField", + }, + ) tree = snap.get("structuredContent", snap).get("tree_markdown", "") print(f"\n filtered tree:\n{tree}") @@ -299,19 +307,23 @@ def test_02_type_text_chars(self) -> None: idx = None for line in tree.split("\n"): if "AXTextField" in line and "smart search field" not in line: - m = re.search(r'\[(\d+)\]', line) + m = re.search(r"\[(\d+)\]", line) if m: idx = int(m.group(1)) break if idx is None: - snap = c.call_tool("get_window_state", { - "pid": self._safari_pid, "window_id": window_id, - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._safari_pid, + "window_id": window_id, + }, + ) tree = snap.get("structuredContent", snap).get("tree_markdown", "") for line in tree.split("\n"): if "AXTextField" in line and "smart search field" not in line: - m = re.search(r'\[(\d+)\]', line) + m = re.search(r"\[(\d+)\]", line) if m: idx = int(m.group(1)) break @@ -320,18 +332,24 @@ def test_02_type_text_chars(self) -> None: print(f" text field element_index: {idx}") # Focus the text field via AX click first. - c.call_tool("click", { - "pid": self._safari_pid, - "window_id": window_id, - "element_index": idx, - }) + c.call_tool( + "click", + { + "pid": self._safari_pid, + "window_id": window_id, + "element_index": idx, + }, + ) time.sleep(0.3) # Type via keystroke synthesis. - result = c.call_tool("type_text_chars", { - "pid": self._safari_pid, - "text": "hello bg", - }) + result = c.call_tool( + "type_text_chars", + { + "pid": self._safari_pid, + "text": "hello bg", + }, + ) print(f" type_text_chars result: {result}") has_text = False @@ -339,7 +357,7 @@ def test_02_type_text_chars(self) -> None: time.sleep(0.5) with DriverClient(self.binary) as c: tree = _get_page_text(c, self._safari_pid) - has_text = ("hello bg" in tree) + has_text = "hello bg" in tree if has_text: break print(f" attempt {attempt+1}: text not yet visible in tree") @@ -353,11 +371,14 @@ def test_03_pixel_click_no_focus_steal(self) -> None: """Pixel-click backgrounded Safari — verify no focus steal.""" with DriverClient(self.binary) as c: window_id = resolve_window_id(c, self._safari_pid) - snap = c.call_tool("get_window_state", { - "pid": self._safari_pid, - "window_id": window_id, - "query": "Click Me", - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._safari_pid, + "window_id": window_id, + "query": "Click Me", + }, + ) sc = snap.get("structuredContent", snap) width = sc.get("screenshot_width", 0) height = sc.get("screenshot_height", 0) @@ -367,12 +388,15 @@ def test_03_pixel_click_no_focus_steal(self) -> None: y = int(height * 0.35) print(f" pixel click at ({x}, {y})") - result = c.call_tool("click", { - "pid": self._safari_pid, - "window_id": window_id, - "x": x, - "y": y, - }) + result = c.call_tool( + "click", + { + "pid": self._safari_pid, + "window_id": window_id, + "x": x, + "y": y, + }, + ) print(f" click result: {result}") time.sleep(0.5) @@ -381,9 +405,13 @@ def test_03_pixel_click_no_focus_steal(self) -> None: def test_04_press_key_tab(self) -> None: """Send Tab key to backgrounded Safari without stealing focus.""" with DriverClient(self.binary) as c: - result = c.call_tool("press_key", { - "pid": self._safari_pid, "key": "tab", - }) + result = c.call_tool( + "press_key", + { + "pid": self._safari_pid, + "key": "tab", + }, + ) print(f"\n press_key result: {result}") time.sleep(0.3) @@ -394,6 +422,7 @@ def test_04_press_key_tab(self) -> None: # Calculator AX-click background tests # --------------------------------------------------------------------------- + class CalculatorBackgroundClickTests(unittest.TestCase): """AX-click backgrounded Calculator without stealing focus.""" @@ -429,13 +458,13 @@ def setUpClass(cls) -> None: with DriverClient(cls.binary) as c: active = frontmost_bundle_id(c) - assert active == FOCUS_MONITOR_BUNDLE, ( - f"Expected FocusMonitorApp frontmost, got {active}" - ) + assert ( + active == FOCUS_MONITOR_BUNDLE + ), f"Expected FocusMonitorApp frontmost, got {active}" @classmethod def tearDownClass(cls) -> None: - if hasattr(cls, '_focus_proc'): + if hasattr(cls, "_focus_proc"): cls._focus_proc.terminate() try: cls._focus_proc.wait(timeout=3) @@ -461,12 +490,14 @@ def _assert_no_focus_loss(self, label: str) -> None: with DriverClient(self.binary) as c: active = frontmost_bundle_id(c) loss_delta = losses - self._losses_before - print(f" [{label}] losses: {self._losses_before}->{losses} " - f"(delta={loss_delta}), frontmost: {active}") + print( + f" [{label}] losses: {self._losses_before}->{losses} " + f"(delta={loss_delta}), frontmost: {active}" + ) self.assertEqual( - active, FOCUS_MONITOR_BUNDLE, - f"[{label}] Focus not restored — " - f"frontmost is {active}, not FocusMonitorApp", + active, + FOCUS_MONITOR_BUNDLE, + f"[{label}] Focus not restored — " f"frontmost is {active}, not FocusMonitorApp", ) def test_01_ax_click_2_plus_2(self) -> None: @@ -490,18 +521,24 @@ def test_01_ax_click_2_plus_2(self) -> None: self.assertIsNotNone(btn_eq, "'Equals' button not found") for idx in [btn_2, btn_add, btn_2, btn_eq]: - c.call_tool("click", { - "pid": self._calc_pid, - "window_id": window_id, - "element_index": idx, - }) + c.call_tool( + "click", + { + "pid": self._calc_pid, + "window_id": window_id, + "element_index": idx, + }, + ) time.sleep(0.3) - snap = c.call_tool("get_window_state", { - "pid": self._calc_pid, - "window_id": window_id, - "query": "AXStaticText", - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._calc_pid, + "window_id": window_id, + "query": "AXStaticText", + }, + ) tree = snap.get("structuredContent", snap).get("tree_markdown", "") print(f" result tree:\n{tree}") @@ -513,25 +550,36 @@ def test_02_ax_click_3_plus_4(self) -> None: with DriverClient(self.binary) as c: window_id = resolve_window_id(c, self._calc_pid) # Clear first - snap = c.call_tool("get_window_state", { - "pid": self._calc_pid, "window_id": window_id, - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._calc_pid, + "window_id": window_id, + }, + ) tree = snap.get("structuredContent", snap).get("tree_markdown", "") btn_c = _find_calc_button(tree, "All Clear") if btn_c is None: btn_c = _find_calc_button(tree, "Clear") if btn_c is not None: - c.call_tool("click", { - "pid": self._calc_pid, - "window_id": window_id, - "element_index": btn_c, - }) + c.call_tool( + "click", + { + "pid": self._calc_pid, + "window_id": window_id, + "element_index": btn_c, + }, + ) time.sleep(0.3) # Find buttons for 3+4= - snap = c.call_tool("get_window_state", { - "pid": self._calc_pid, "window_id": window_id, - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._calc_pid, + "window_id": window_id, + }, + ) tree = snap.get("structuredContent", snap).get("tree_markdown", "") btn_3 = _find_calc_button(tree, "3") btn_add = _find_calc_button(tree, "Add") @@ -545,18 +593,24 @@ def test_02_ax_click_3_plus_4(self) -> None: self.assertIsNotNone(btn_eq, "'Equals' button not found") for idx in [btn_3, btn_add, btn_4, btn_eq]: - c.call_tool("click", { - "pid": self._calc_pid, - "window_id": window_id, - "element_index": idx, - }) + c.call_tool( + "click", + { + "pid": self._calc_pid, + "window_id": window_id, + "element_index": idx, + }, + ) time.sleep(0.3) - snap = c.call_tool("get_window_state", { - "pid": self._calc_pid, - "window_id": window_id, - "query": "AXStaticText", - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._calc_pid, + "window_id": window_id, + "query": "AXStaticText", + }, + ) tree = snap.get("structuredContent", snap).get("tree_markdown", "") print(f" result tree:\n{tree}") diff --git a/libs/cua-driver-rs/tests/integration/test_cli.py b/libs/cua-driver-rs/tests/integration/test_cli.py index 152e5c7a81..51b29e6f38 100644 --- a/libs/cua-driver-rs/tests/integration/test_cli.py +++ b/libs/cua-driver-rs/tests/integration/test_cli.py @@ -133,14 +133,19 @@ class ServeDaemonTests(unittest.TestCase): @classmethod def setUpClass(cls) -> None: cls.binary = default_binary_path() - import tempfile, os + import os + import tempfile + cls._sock_file = tempfile.mktemp(suffix=".sock", prefix="cua-driver-test-") def _start_daemon(self): - import subprocess, time + import subprocess + import time + proc = subprocess.Popen( [self.binary, "serve", "--socket", self._sock_file], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, ) # Wait for daemon to bind. for _ in range(30): @@ -152,6 +157,7 @@ def _start_daemon(self): def test_status_exits_1_when_no_daemon(self) -> None: import tempfile + sock = tempfile.mktemp(suffix=".sock", prefix="cua-noexist-") r = _run([self.binary, "status", "--socket", sock]) self.assertEqual(r.returncode, 1) @@ -159,6 +165,7 @@ def test_status_exits_1_when_no_daemon(self) -> None: def test_stop_exits_1_when_no_daemon(self) -> None: import tempfile + sock = tempfile.mktemp(suffix=".sock", prefix="cua-noexist2-") r = _run([self.binary, "stop", "--socket", sock]) self.assertEqual(r.returncode, 1) @@ -178,7 +185,9 @@ def test_serve_status_stop_lifecycle(self) -> None: self.assertEqual(r.returncode, 0, f"stop stderr: {r.stderr}") # Status should now show not running. - import time; time.sleep(0.2) + import time + + time.sleep(0.2) r = _run([self.binary, "status", "--socket", self._sock_file]) self.assertEqual(r.returncode, 1) finally: diff --git a/libs/cua-driver-rs/tests/integration/test_concurrent_drivers.py b/libs/cua-driver-rs/tests/integration/test_concurrent_drivers.py index 83585af4ae..7229c35c7a 100644 --- a/libs/cua-driver-rs/tests/integration/test_concurrent_drivers.py +++ b/libs/cua-driver-rs/tests/integration/test_concurrent_drivers.py @@ -46,9 +46,7 @@ def _build_focus_app() -> None: if not os.path.exists(_FOCUS_APP_EXE): - subprocess.run( - [os.path.join(_FOCUS_APP_DIR, "build.sh")], check=True - ) + subprocess.run([os.path.join(_FOCUS_APP_DIR, "build.sh")], check=True) def _launch_focus_app() -> tuple[subprocess.Popen, int]: @@ -79,10 +77,10 @@ def _find_calc_button(tree: str, label: str) -> int | None: for line in tree.split("\n"): if "AXButton" not in line: continue - m = re.search(r'\[(\d+)\]', line) + m = re.search(r"\[(\d+)\]", line) if not m: continue - if f'({label})' in line or f'id={label}' in line: + if f"({label})" in line or f"id={label}" in line: return int(m.group(1)) return None @@ -125,16 +123,16 @@ def setUpClass(cls) -> None: with DriverClient(cls.binary) as c: active = frontmost_bundle_id(c) - assert active == FOCUS_MONITOR_BUNDLE, ( - f"Expected FocusMonitorApp frontmost, got {active}" - ) + assert ( + active == FOCUS_MONITOR_BUNDLE + ), f"Expected FocusMonitorApp frontmost, got {active}" losses = _read_focus_losses() assert losses == 0, f"Expected 0 focus losses at start, got {losses}" @classmethod def tearDownClass(cls) -> None: - if hasattr(cls, '_focus_proc'): + if hasattr(cls, "_focus_proc"): cls._focus_proc.terminate() try: cls._focus_proc.wait(timeout=3) @@ -153,15 +151,24 @@ def setUp(self) -> None: # Clear Calculator before each test so previous display state doesn't interfere. with DriverClient(self.binary) as c: window_id = resolve_window_id(c, self._calc_pid) - snap = c.call_tool("get_window_state", { - "pid": self._calc_pid, "window_id": window_id, - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._calc_pid, + "window_id": window_id, + }, + ) tree = snap.get("structuredContent", snap).get("tree_markdown", "") btn_ac = _find_calc_button(tree, "All Clear") or _find_calc_button(tree, "Clear") if btn_ac is not None: - c.call_tool("click", { - "pid": self._calc_pid, "window_id": window_id, "element_index": btn_ac, - }) + c.call_tool( + "click", + { + "pid": self._calc_pid, + "window_id": window_id, + "element_index": btn_ac, + }, + ) time.sleep(0.2) subprocess.run( @@ -186,9 +193,13 @@ def run_driver_a() -> None: with DriverClient(self.binary) as ca: # Driver A: AX-click 2 + 2 = on Calculator. window_id = resolve_window_id(ca, self._calc_pid) - snap = ca.call_tool("get_window_state", { - "pid": self._calc_pid, "window_id": window_id, - }) + snap = ca.call_tool( + "get_window_state", + { + "pid": self._calc_pid, + "window_id": window_id, + }, + ) tree = snap.get("structuredContent", snap).get("tree_markdown", "") btn_2 = _find_calc_button(tree, "2") btn_add = _find_calc_button(tree, "Add") @@ -197,16 +208,23 @@ def run_driver_a() -> None: driver_a_error.append("Missing calculator buttons") return for idx in [btn_2, btn_add, btn_2, btn_eq]: - ca.call_tool("click", { + ca.call_tool( + "click", + { + "pid": self._calc_pid, + "window_id": window_id, + "element_index": idx, + }, + ) + time.sleep(0.15) + snap2 = ca.call_tool( + "get_window_state", + { "pid": self._calc_pid, "window_id": window_id, - "element_index": idx, - }) - time.sleep(0.15) - snap2 = ca.call_tool("get_window_state", { - "pid": self._calc_pid, "window_id": window_id, - "query": "AXStaticText", - }) + "query": "AXStaticText", + }, + ) tree2 = snap2.get("structuredContent", snap2).get("tree_markdown", "") driver_a_result["tree"] = tree2 except Exception as e: @@ -266,7 +284,8 @@ def run_driver_b() -> None: active = frontmost_bundle_id(c) print(f" losses: {self._losses_before}->{losses}, frontmost: {active}") self.assertEqual( - active, FOCUS_MONITOR_BUNDLE, + active, + FOCUS_MONITOR_BUNDLE, f"Focus stolen during concurrent operation — frontmost is {active}", ) @@ -276,15 +295,24 @@ def test_sequential_driver_reuse(self) -> None: # First session — clear Calculator. with DriverClient(self.binary) as c: window_id = resolve_window_id(c, self._calc_pid) - snap = c.call_tool("get_window_state", { - "pid": self._calc_pid, "window_id": window_id, - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._calc_pid, + "window_id": window_id, + }, + ) tree = snap.get("structuredContent", snap).get("tree_markdown", "") btn_ac = _find_calc_button(tree, "All Clear") or _find_calc_button(tree, "Clear") if btn_ac is not None: - c.call_tool("click", { - "pid": self._calc_pid, "window_id": window_id, "element_index": btn_ac, - }) + c.call_tool( + "click", + { + "pid": self._calc_pid, + "window_id": window_id, + "element_index": btn_ac, + }, + ) time.sleep(0.2) time.sleep(0.2) @@ -292,19 +320,33 @@ def test_sequential_driver_reuse(self) -> None: # Second session — press 5. with DriverClient(self.binary) as c: window_id = resolve_window_id(c, self._calc_pid) - snap = c.call_tool("get_window_state", { - "pid": self._calc_pid, "window_id": window_id, - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._calc_pid, + "window_id": window_id, + }, + ) tree = snap.get("structuredContent", snap).get("tree_markdown", "") btn_5 = _find_calc_button(tree, "5") self.assertIsNotNone(btn_5, "Could not find '5' button in fresh session") - c.call_tool("click", { - "pid": self._calc_pid, "window_id": window_id, "element_index": btn_5, - }) + c.call_tool( + "click", + { + "pid": self._calc_pid, + "window_id": window_id, + "element_index": btn_5, + }, + ) time.sleep(0.3) - snap2 = c.call_tool("get_window_state", { - "pid": self._calc_pid, "window_id": window_id, "query": "AXStaticText", - }) + snap2 = c.call_tool( + "get_window_state", + { + "pid": self._calc_pid, + "window_id": window_id, + "query": "AXStaticText", + }, + ) tree2 = snap2.get("structuredContent", snap2).get("tree_markdown", "") print(f"\n After pressing 5: {tree2[:200]}") @@ -315,7 +357,8 @@ def test_sequential_driver_reuse(self) -> None: with DriverClient(self.binary) as c: active = frontmost_bundle_id(c) self.assertEqual( - active, FOCUS_MONITOR_BUNDLE, + active, + FOCUS_MONITOR_BUNDLE, f"Focus stolen — frontmost is {active}", ) diff --git a/libs/cua-driver-rs/tests/integration/test_cursor_visibility.py b/libs/cua-driver-rs/tests/integration/test_cursor_visibility.py index 910b5e0b2c..b0721567ca 100644 --- a/libs/cua-driver-rs/tests/integration/test_cursor_visibility.py +++ b/libs/cua-driver-rs/tests/integration/test_cursor_visibility.py @@ -36,10 +36,10 @@ CALCULATOR_BUNDLE = "com.apple.calculator" # Colour tolerances for detection -_MAGENTA_MIN = (200, 0, 200) # R≥200, G≤55, B≥200 +_MAGENTA_MIN = (200, 0, 200) # R≥200, G≤55, B≥200 _MAGENTA_MAX = (255, 55, 255) -_YELLOW_MIN = (200, 200, 0) # R≥200, G≥200, B≤55 -_YELLOW_MAX = (255, 255, 55) +_YELLOW_MIN = (200, 200, 0) # R≥200, G≥200, B≤55 +_YELLOW_MAX = (255, 255, 55) # Minimum fraction of pixels in the search region that must match the cursor colours. _MATCH_THRESHOLD = 0.002 # 0.2% of the search window @@ -51,7 +51,7 @@ def _decode_png_pixels(png_bytes: bytes) -> tuple[int, int, list[tuple[int, int, Only supports filter types 0 (None) and 1 (Sub) — which covers the screenshots produced by cua-driver's `screenshot` tool. Raises ValueError for unsupported formats. """ - if png_bytes[:8] != b'\x89PNG\r\n\x1a\n': + if png_bytes[:8] != b"\x89PNG\r\n\x1a\n": raise ValueError("Not a PNG file") pos = 8 @@ -62,18 +62,18 @@ def _decode_png_pixels(png_bytes: bytes) -> tuple[int, int, list[tuple[int, int, while pos < len(png_bytes): if pos + 8 > len(png_bytes): break - length = struct.unpack('>I', png_bytes[pos:pos+4])[0] - chunk_type = png_bytes[pos+4:pos+8] - data = png_bytes[pos+8:pos+8+length] + length = struct.unpack(">I", png_bytes[pos : pos + 4])[0] + chunk_type = png_bytes[pos + 4 : pos + 8] + data = png_bytes[pos + 8 : pos + 8 + length] pos += 12 + length - if chunk_type == b'IHDR': - width, height = struct.unpack('>II', data[:8]) + if chunk_type == b"IHDR": + width, height = struct.unpack(">II", data[:8]) bit_depth = data[8] color_type = data[9] - elif chunk_type == b'IDAT': + elif chunk_type == b"IDAT": idat_chunks.append(data) - elif chunk_type == b'IEND': + elif chunk_type == b"IEND": break if width == 0 or height == 0: @@ -81,7 +81,7 @@ def _decode_png_pixels(png_bytes: bytes) -> tuple[int, int, list[tuple[int, int, if bit_depth != 8: raise ValueError(f"Unsupported bit depth: {bit_depth}") - raw = zlib.decompress(b''.join(idat_chunks)) + raw = zlib.decompress(b"".join(idat_chunks)) # Channels: 2=RGB(3), 6=RGBA(4) channels = {2: 3, 6: 4}.get(color_type) @@ -96,7 +96,7 @@ def _decode_png_pixels(png_bytes: bytes) -> tuple[int, int, list[tuple[int, int, for _y in range(height): filt = raw[offset] offset += 1 - row = bytearray(raw[offset:offset+stride]) + row = bytearray(raw[offset : offset + stride]) offset += stride if filt == 0: @@ -118,14 +118,16 @@ def _decode_png_pixels(png_bytes: bytes) -> tuple[int, int, list[tuple[int, int, b = prev_row[i] c = prev_row[i - channels] if i >= channels else 0 p = a + b - c - pa = abs(p - a); pb = abs(p - b); pc = abs(p - c) + pa = abs(p - a) + pb = abs(p - b) + pc = abs(p - c) pr = a if pa <= pb and pa <= pc else (b if pb <= pc else c) row[i] = (row[i] + pr) & 0xFF else: raise ValueError(f"Unknown PNG filter type: {filt}") for i in range(0, stride, channels): - pixels.append((row[i], row[i+1], row[i+2])) + pixels.append((row[i], row[i + 1], row[i + 2])) prev_row = bytes(row) return width, height, pixels @@ -156,9 +158,11 @@ def _colour_in_region( continue r, g, b = pixels[idx] total += 1 - if (colour_min[0] <= r <= colour_max[0] and - colour_min[1] <= g <= colour_max[1] and - colour_min[2] <= b <= colour_max[2]): + if ( + colour_min[0] <= r <= colour_max[0] + and colour_min[1] <= g <= colour_max[1] + and colour_min[2] <= b <= colour_max[2] + ): matches += 1 return matches / total if total > 0 else 0.0 @@ -212,7 +216,9 @@ def test_custom_cursor_visible_after_click(self) -> None: # 4. Resolve the Calculator window and its screen bounds. win_id = resolve_window_id(client, self.calc_pid) - wins = client.call_tool("list_windows", {"pid": self.calc_pid})["structuredContent"]["windows"] + wins = client.call_tool("list_windows", {"pid": self.calc_pid})["structuredContent"][ + "windows" + ] win = next((w for w in wins if w["window_id"] == win_id), None) self.assertIsNotNone(win, "Could not find Calculator window") bounds = win["bounds"] @@ -220,12 +226,15 @@ def test_custom_cursor_visible_after_click(self) -> None: # 5. Click the center of the Calculator window (window-local coords). click_x = bounds["width"] / 2.0 click_y = bounds["height"] / 2.0 - r = client.call_tool("click", { - "pid": self.calc_pid, - "window_id": win_id, - "x": click_x, - "y": click_y, - }) + r = client.call_tool( + "click", + { + "pid": self.calc_pid, + "window_id": win_id, + "x": click_x, + "y": click_y, + }, + ) self.assertFalse(r.get("isError"), f"click failed: {r}") # 6. Compute expected screen coordinates of the click point. @@ -242,7 +251,7 @@ def test_custom_cursor_visible_after_click(self) -> None: if b64: png = base64.b64decode(b64) # PNG width is at bytes 16..20 in big-endian. - png_w = struct.unpack('>I', png[16:20])[0] + png_w = struct.unpack(">I", png[16:20])[0] scale = png_w / bounds["width"] if bounds["width"] > 0 else 1.0 else: scale = 1.0 @@ -274,16 +283,29 @@ def test_custom_cursor_visible_after_click(self) -> None: search_r = int(80 * ss_scale) magenta_frac = _colour_in_region( - pixels, img_w, img_h, sx, sy, search_r, - _MAGENTA_MIN, _MAGENTA_MAX, + pixels, + img_w, + img_h, + sx, + sy, + search_r, + _MAGENTA_MIN, + _MAGENTA_MAX, ) yellow_frac = _colour_in_region( - pixels, img_w, img_h, sx, sy, search_r, - _YELLOW_MIN, _YELLOW_MAX, + pixels, + img_w, + img_h, + sx, + sy, + search_r, + _YELLOW_MIN, + _YELLOW_MAX, ) self.assertGreater( - magenta_frac, _MATCH_THRESHOLD, + magenta_frac, + _MATCH_THRESHOLD, f"Magenta cursor colour NOT found near click point " f"(screen={screen_x:.0f},{screen_y:.0f} img={sx:.0f},{sy:.0f} " f"scale={ss_scale:.2f}). " @@ -292,7 +314,8 @@ def test_custom_cursor_visible_after_click(self) -> None: f"The overlay may be hidden or behind another window.", ) self.assertGreater( - yellow_frac, _MATCH_THRESHOLD, + yellow_frac, + _MATCH_THRESHOLD, f"Yellow cross NOT found in cursor region " f"(magenta={magenta_frac:.5f}, yellow={yellow_frac:.5f}). " f"Cursor shape may not have loaded correctly.", @@ -321,7 +344,9 @@ def test_default_cursor_visible_after_click(self) -> None: # 3. Resolve window and screen bounds. win_id = resolve_window_id(client, self.calc_pid) - wins = client.call_tool("list_windows", {"pid": self.calc_pid})["structuredContent"]["windows"] + wins = client.call_tool("list_windows", {"pid": self.calc_pid})["structuredContent"][ + "windows" + ] win = next((w for w in wins if w["window_id"] == win_id), None) self.assertIsNotNone(win, "Could not find Calculator window") bounds = win["bounds"] @@ -334,18 +359,21 @@ def test_default_cursor_visible_after_click(self) -> None: scr = client.call_tool("screenshot", {"pid": self.calc_pid, "window_id": win_id}) b64 = scr.get("content", [{}])[0].get("data", "") png = base64.b64decode(b64) if b64 else b"" - png_w = struct.unpack('>I', png[16:20])[0] if len(png) >= 24 else 0 + png_w = struct.unpack(">I", png[16:20])[0] if len(png) >= 24 else 0 scale = png_w / bounds["width"] if bounds["width"] > 0 and png_w > 0 else 1.0 except Exception: scale = 1.0 # 4. Click the centre of the window. - r = client.call_tool("click", { - "pid": self.calc_pid, - "window_id": win_id, - "x": click_x, - "y": click_y, - }) + r = client.call_tool( + "click", + { + "pid": self.calc_pid, + "window_id": win_id, + "x": click_x, + "y": click_y, + }, + ) self.assertFalse(r.get("isError"), f"click failed: {r}") # 5. Wait for 750ms glide + spring settle + a safety margin. @@ -373,16 +401,23 @@ def test_default_cursor_visible_after_click(self) -> None: # Default arrow: ice-blue/cyan — cursor_mid ≈ (94, 192, 232), bloom ≈ (188, 232, 252). # Accept any pixel that is clearly blue-cyan (G and B significantly exceed R). - _CYAN_MIN = (40, 150, 180) + _CYAN_MIN = (40, 150, 180) _CYAN_MAX = (180, 255, 255) cyan_frac = _colour_in_region( - pixels, img_w, img_h, sx, sy, search_r, - _CYAN_MIN, _CYAN_MAX, + pixels, + img_w, + img_h, + sx, + sy, + search_r, + _CYAN_MIN, + _CYAN_MAX, ) self.assertGreater( - cyan_frac, _MATCH_THRESHOLD, + cyan_frac, + _MATCH_THRESHOLD, f"Default ice-blue cursor NOT found near click point " f"(screen={screen_x:.0f},{screen_y:.0f} img_pt={sx:.0f},{sy:.0f} " f"scale={ss_scale:.2f}). " @@ -392,10 +427,13 @@ def test_default_cursor_visible_after_click(self) -> None: def test_set_agent_cursor_style_gradient_only(self) -> None: """set_agent_cursor_style with gradient_colors updates without error.""" - r = self.client.call_tool("set_agent_cursor_style", { - "gradient_colors": ["#FF0000", "#FF00FF", "#0000FF"], - "bloom_color": "#00FFFF", - }) + r = self.client.call_tool( + "set_agent_cursor_style", + { + "gradient_colors": ["#FF0000", "#FF00FF", "#0000FF"], + "bloom_color": "#00FFFF", + }, + ) self.assertFalse(r.get("isError"), f"gradient-only style update failed: {r}") text = r.get("content", [{}])[0].get("text", "") self.assertIn("✅", text) diff --git a/libs/cua-driver-rs/tests/integration/test_double_click_delivery.py b/libs/cua-driver-rs/tests/integration/test_double_click_delivery.py index f272bd859d..244d9761b5 100644 --- a/libs/cua-driver-rs/tests/integration/test_double_click_delivery.py +++ b/libs/cua-driver-rs/tests/integration/test_double_click_delivery.py @@ -48,9 +48,7 @@ def _build_focus_app() -> None: if not os.path.exists(_FOCUS_APP_EXE): - subprocess.run( - [os.path.join(_FOCUS_APP_DIR, "build.sh")], check=True - ) + subprocess.run([os.path.join(_FOCUS_APP_DIR, "build.sh")], check=True) def _launch_focus_app() -> tuple[subprocess.Popen, int]: @@ -143,15 +141,15 @@ def setUpClass(cls) -> None: with DriverClient(cls.binary) as c: active = frontmost_bundle_id(c) - assert active == FOCUS_MONITOR_BUNDLE, ( - f"Expected FocusMonitorApp frontmost, got {active}" - ) + assert ( + active == FOCUS_MONITOR_BUNDLE + ), f"Expected FocusMonitorApp frontmost, got {active}" losses = _read_focus_losses() assert losses == 0, f"Expected 0 focus losses at start, got {losses}" @classmethod def tearDownClass(cls) -> None: - if hasattr(cls, '_focus_proc'): + if hasattr(cls, "_focus_proc"): cls._focus_proc.terminate() try: cls._focus_proc.wait(timeout=3) @@ -174,11 +172,14 @@ def setUp(self) -> None: tree = snap.get("structuredContent", snap).get("tree_markdown", "") btn = _find_calc_button(tree, "All Clear") or _find_calc_button(tree, "Clear") if btn is not None: - c.call_tool("click", { - "pid": self._calc_pid, - "window_id": window_id, - "element_index": btn, - }) + c.call_tool( + "click", + { + "pid": self._calc_pid, + "window_id": window_id, + "element_index": btn, + }, + ) time.sleep(0.3) subprocess.run( ["osascript", "-e", 'tell application "FocusMonitorApp" to activate'], @@ -194,7 +195,8 @@ def _assert_no_focus_steal(self) -> None: active = frontmost_bundle_id(c) print(f" losses: {self._losses_before}->{losses}, frontmost: {active}") self.assertEqual( - active, FOCUS_MONITOR_BUNDLE, + active, + FOCUS_MONITOR_BUNDLE, f"Focus stolen — frontmost is {active}", ) @@ -202,18 +204,25 @@ def test_element_index_double_click_no_focus_steal(self) -> None: """double_click via element_index on Calculator '2' button — no focus steal.""" with DriverClient(self.binary) as c: window_id = resolve_window_id(c, self._calc_pid) - snap = c.call_tool("get_window_state", { - "pid": self._calc_pid, "window_id": window_id, - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._calc_pid, + "window_id": window_id, + }, + ) tree = snap.get("structuredContent", snap).get("tree_markdown", "") two_button = _find_calc_button(tree, "2") self.assertIsNotNone(two_button, "Could not locate Calculator's '2' AXButton") - result = c.call_tool("double_click", { - "pid": self._calc_pid, - "window_id": window_id, - "element_index": two_button, - }) + result = c.call_tool( + "double_click", + { + "pid": self._calc_pid, + "window_id": window_id, + "element_index": two_button, + }, + ) self.assertIsNone(result.get("isError"), msg=result) text = result.get("content", [{}])[0].get("text", "") print(f"\n double_click result: {text}") @@ -225,9 +234,13 @@ def test_pixel_double_click_no_focus_steal(self) -> None: """double_click via pixel coords — no focus steal.""" with DriverClient(self.binary) as c: window_id = resolve_window_id(c, self._calc_pid) - snap = c.call_tool("get_window_state", { - "pid": self._calc_pid, "window_id": window_id, - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._calc_pid, + "window_id": window_id, + }, + ) sc = snap.get("structuredContent", snap) w = sc.get("screenshot_width", CALC_DEFAULT_W) h = sc.get("screenshot_height", CALC_DEFAULT_H) @@ -235,12 +248,15 @@ def test_pixel_double_click_no_focus_steal(self) -> None: bx, by = CALC_TWO_BUTTON_XY x = int(bx * w / CALC_DEFAULT_W) y = int(by * h / CALC_DEFAULT_H) - result = c.call_tool("double_click", { - "pid": self._calc_pid, - "window_id": window_id, - "x": x, - "y": y, - }) + result = c.call_tool( + "double_click", + { + "pid": self._calc_pid, + "window_id": window_id, + "x": x, + "y": y, + }, + ) self.assertIsNone(result.get("isError"), msg=result) text = result.get("content", [{}])[0].get("text", "") print(f"\n double_click pixel result: {text}") diff --git a/libs/cua-driver-rs/tests/integration/test_focus_steal_parity.py b/libs/cua-driver-rs/tests/integration/test_focus_steal_parity.py index 5f4dcc5a18..05669cca6a 100644 --- a/libs/cua-driver-rs/tests/integration/test_focus_steal_parity.py +++ b/libs/cua-driver-rs/tests/integration/test_focus_steal_parity.py @@ -217,9 +217,7 @@ def test_launch_with_url_preserves_frontmost(self) -> None: time.sleep(0.5) self.assertEqual(_frontmost_bundle_id(), self.PRIOR_BUNDLE) - result = self._launch( - {"bundle_id": "com.apple.Safari", "urls": ["about:blank"]} - ) + result = self._launch({"bundle_id": "com.apple.Safari", "urls": ["about:blank"]}) # NOTE: the Swift binary on macOS Sonoma+ fails this launch with # "The application 'Safari' could not be launched because it was # not found" — that's a Cryptex-app + URL-handoff regression on diff --git a/libs/cua-driver-rs/tests/integration/test_list_windows.py b/libs/cua-driver-rs/tests/integration/test_list_windows.py index a9f5c8d59b..79ee004fc5 100644 --- a/libs/cua-driver-rs/tests/integration/test_list_windows.py +++ b/libs/cua-driver-rs/tests/integration/test_list_windows.py @@ -25,7 +25,6 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from driver_client import DriverClient, default_binary_path - CALCULATOR_BUNDLE = "com.apple.calculator" @@ -53,13 +52,17 @@ def _call_list_windows(self, **args): def test_default_returns_layer_zero_windows_with_field_names(self): body = self._call_list_windows() windows = body["windows"] - self.assertGreater( - len(windows), 0, "expected at least Calculator's window" - ) + self.assertGreater(len(windows), 0, "expected at least Calculator's window") required = { - "window_id", "pid", "app_name", "title", "bounds", - "layer", "z_index", "is_on_screen", + "window_id", + "pid", + "app_name", + "title", + "bounds", + "layer", + "z_index", + "is_on_screen", } for w in windows: self.assertTrue( @@ -71,9 +74,7 @@ def test_default_returns_layer_zero_windows_with_field_names(self): def test_pid_filter_narrows_to_one_pid(self): body = self._call_list_windows(pid=self.calc_pid) windows = body["windows"] - self.assertGreater( - len(windows), 0, "Calculator should have at least one window" - ) + self.assertGreater(len(windows), 0, "Calculator should have at least one window") self.assertTrue( all(w["pid"] == self.calc_pid for w in windows), "pid filter leaked other pids into the result", @@ -103,7 +104,8 @@ def test_bounds_fields_present_and_non_negative(self): for field in ("x", "y", "width", "height"): self.assertIn(field, b, f"bounds missing '{field}'") self.assertGreaterEqual( - b[field], 0, + b[field], + 0, f"bounds.{field} should be non-negative, got {b[field]}", ) @@ -112,7 +114,8 @@ def test_z_index_positive(self): windows = body["windows"] for w in windows: self.assertGreater( - w["z_index"], 0, + w["z_index"], + 0, f"z_index should be > 0 (front-to-back ordering), got {w['z_index']}", ) diff --git a/libs/cua-driver-rs/tests/integration/test_overlay_z_order.py b/libs/cua-driver-rs/tests/integration/test_overlay_z_order.py index e7121daf0f..f55328a83f 100644 --- a/libs/cua-driver-rs/tests/integration/test_overlay_z_order.py +++ b/libs/cua-driver-rs/tests/integration/test_overlay_z_order.py @@ -96,7 +96,7 @@ def setUp(self) -> None: time.sleep(0.5) def tearDown(self) -> None: - if hasattr(self, '_focus_proc'): + if hasattr(self, "_focus_proc"): self._focus_proc.terminate() try: self._focus_proc.wait(timeout=3) @@ -126,18 +126,20 @@ def test_overlay_at_normal_level_above_target(self) -> None: cy = b["height"] / 2.0 fg_wins_before = [ - w for w in all_before - if w["z_index"] > calc_z_before - and w["pid"] != driver_pid - and w.get("is_on_screen") + w + for w in all_before + if w["z_index"] > calc_z_before and w["pid"] != driver_pid and w.get("is_on_screen") ] - self.client.call_tool("click", { - "pid": self.calc_pid, - "window_id": calc_win_id, - "x": cx, - "y": cy, - }) + self.client.call_tool( + "click", + { + "pid": self.calc_pid, + "window_id": calc_win_id, + "x": cx, + "y": cy, + }, + ) # Let defensive-repin ticks fully settle. time.sleep(1.5) @@ -154,9 +156,7 @@ def test_overlay_at_normal_level_above_target(self) -> None: overlay_z = max(w["z_index"] for w in overlay_wins) calc_wins_after = [w for w in all_after if w["pid"] == self.calc_pid] - calc_z_after = ( - max(w["z_index"] for w in calc_wins_after) if calc_wins_after else 0 - ) + calc_z_after = max(w["z_index"] for w in calc_wins_after) if calc_wins_after else 0 self.assertGreater( overlay_z, calc_z_after, diff --git a/libs/cua-driver-rs/tests/integration/test_pixel_click_delivery.py b/libs/cua-driver-rs/tests/integration/test_pixel_click_delivery.py index 985047e868..04fcadf39e 100644 --- a/libs/cua-driver-rs/tests/integration/test_pixel_click_delivery.py +++ b/libs/cua-driver-rs/tests/integration/test_pixel_click_delivery.py @@ -46,15 +46,15 @@ # Column centers (x): 29, 86, 144, 201 # Row centers (y): 133, 193, 253, 313, 373 CALC_BUTTONS = { - "1": (29, 313), "2": (86, 313), "3": (144, 313), + "1": (29, 313), + "2": (86, 313), + "3": (144, 313), } def _build_focus_app() -> None: if not os.path.exists(_FOCUS_APP_EXE): - subprocess.run( - [os.path.join(_FOCUS_APP_DIR, "build.sh")], check=True - ) + subprocess.run([os.path.join(_FOCUS_APP_DIR, "build.sh")], check=True) def _launch_focus_app() -> tuple[subprocess.Popen, int]: @@ -85,10 +85,10 @@ def _find_calc_button(tree: str, label: str) -> int | None: for line in tree.split("\n"): if "AXButton" not in line: continue - m = re.search(r'\[(\d+)\]', line) + m = re.search(r"\[(\d+)\]", line) if not m: continue - if f'({label})' in line or f'id={label}' in line: + if f"({label})" in line or f"id={label}" in line: return int(m.group(1)) return None @@ -141,15 +141,15 @@ def setUpClass(cls) -> None: with DriverClient(cls.binary) as c: active = frontmost_bundle_id(c) - assert active == FOCUS_MONITOR_BUNDLE, ( - f"Expected FocusMonitorApp frontmost, got {active}" - ) + assert ( + active == FOCUS_MONITOR_BUNDLE + ), f"Expected FocusMonitorApp frontmost, got {active}" losses = _read_focus_losses() assert losses == 0, f"Expected 0 focus losses at start, got {losses}" @classmethod def tearDownClass(cls) -> None: - if hasattr(cls, '_focus_proc'): + if hasattr(cls, "_focus_proc"): cls._focus_proc.terminate() try: cls._focus_proc.wait(timeout=3) @@ -173,11 +173,14 @@ def setUp(self) -> None: tree = snap.get("structuredContent", snap).get("tree_markdown", "") btn = _find_calc_button(tree, "All Clear") or _find_calc_button(tree, "Clear") if btn is not None: - c.call_tool("click", { - "pid": self._calc_pid, - "window_id": window_id, - "element_index": btn, - }) + c.call_tool( + "click", + { + "pid": self._calc_pid, + "window_id": window_id, + "element_index": btn, + }, + ) time.sleep(0.3) subprocess.run( ["osascript", "-e", 'tell application "FocusMonitorApp" to activate'], @@ -203,12 +206,15 @@ def test_pixel_click_no_focus_steal(self) -> None: bx, by = CALC_BUTTONS[digit] x = int(bx * w / 230) y = int(by * h / 408) - result = c.call_tool("click", { - "pid": self._calc_pid, - "window_id": window_id, - "x": x, - "y": y, - }) + result = c.call_tool( + "click", + { + "pid": self._calc_pid, + "window_id": window_id, + "x": x, + "y": y, + }, + ) print(f" {digit}: pixel ({x},{y})") time.sleep(0.15) @@ -220,7 +226,8 @@ def test_pixel_click_no_focus_steal(self) -> None: active = frontmost_bundle_id(c) print(f" losses: {self._losses_before}->{losses}, frontmost: {active}") self.assertEqual( - active, FOCUS_MONITOR_BUNDLE, + active, + FOCUS_MONITOR_BUNDLE, f"Focus stolen — frontmost is {active}", ) @@ -228,9 +235,13 @@ def test_ax_click_delivery(self) -> None: """AX-click 1+2 on backgrounded Calculator — verifies AX event delivery.""" with DriverClient(self.binary) as c: window_id = resolve_window_id(c, self._calc_pid) - snap = c.call_tool("get_window_state", { - "pid": self._calc_pid, "window_id": window_id, - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._calc_pid, + "window_id": window_id, + }, + ) tree = snap.get("structuredContent", snap).get("tree_markdown", "") btn_1 = _find_calc_button(tree, "1") @@ -245,18 +256,24 @@ def test_ax_click_delivery(self) -> None: self.assertIsNotNone(btn_eq, "'Equals' button not found") for idx in [btn_1, btn_add, btn_2, btn_eq]: - c.call_tool("click", { - "pid": self._calc_pid, - "window_id": window_id, - "element_index": idx, - }) + c.call_tool( + "click", + { + "pid": self._calc_pid, + "window_id": window_id, + "element_index": idx, + }, + ) time.sleep(0.3) - snap = c.call_tool("get_window_state", { - "pid": self._calc_pid, - "window_id": window_id, - "query": "AXStaticText", - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._calc_pid, + "window_id": window_id, + "query": "AXStaticText", + }, + ) tree = snap.get("structuredContent", snap).get("tree_markdown", "") print(f" result tree:\n{tree}") @@ -267,7 +284,8 @@ def test_ax_click_delivery(self) -> None: active = frontmost_bundle_id(c) print(f" losses: {self._losses_before}->{losses}, frontmost: {active}") self.assertEqual( - active, FOCUS_MONITOR_BUNDLE, + active, + FOCUS_MONITOR_BUNDLE, f"Focus stolen by AX click — frontmost is {active}", ) diff --git a/libs/cua-driver-rs/tests/integration/v2/conftest.py b/libs/cua-driver-rs/tests/integration/v2/conftest.py index 47a163f35e..9481db066e 100644 --- a/libs/cua-driver-rs/tests/integration/v2/conftest.py +++ b/libs/cua-driver-rs/tests/integration/v2/conftest.py @@ -15,10 +15,10 @@ import os import subprocess import sys -import time import threading -import zipfile +import time import urllib.request +import zipfile from http.server import HTTPServer, SimpleHTTPRequestHandler from typing import Optional @@ -62,6 +62,7 @@ # ── binary ──────────────────────────────────────────────────────────────────── + @pytest.fixture(scope="session") def binary() -> str: return default_binary_path() @@ -69,17 +70,20 @@ def binary() -> str: # ── driver ──────────────────────────────────────────────────────────────────── + @pytest.fixture def driver(binary): """Yield a started Driver instance.""" sys.path.insert(0, _HERE) from harness.driver import Driver + with Driver(binary) as d: yield d # ── focus monitor ───────────────────────────────────────────────────────────── + def _build_focus_app() -> None: if not os.path.exists(_FOCUS_APP_EXE): build_sh = os.path.join(_FOCUS_APP_DIR, "build.sh") @@ -152,8 +156,11 @@ def activate_focus_monitor(focus_monitor): """Re-activate FocusMonitorApp before each test (by pid).""" _, pid = focus_monitor subprocess.run( - ["osascript", "-e", - f'tell application "System Events" to set frontmost of (first process whose unix id is {pid}) to true'], + [ + "osascript", + "-e", + f'tell application "System Events" to set frontmost of (first process whose unix id is {pid}) to true', + ], check=False, ) time.sleep(0.4) @@ -161,12 +168,14 @@ def activate_focus_monitor(focus_monitor): # ── UX guard ────────────────────────────────────────────────────────────────── + @pytest.fixture def ux_guard(focus_monitor): """Start UXMonitor before the test, call assert_clean() after.""" _, sentinel_pid = focus_monitor sys.path.insert(0, _HERE) from harness.monitor import UXMonitor + mon = UXMonitor(sentinel_pid=sentinel_pid) mon.start() yield mon @@ -176,6 +185,7 @@ def ux_guard(focus_monitor): # ── local HTML server ───────────────────────────────────────────────────────── + class _SilentHandler(SimpleHTTPRequestHandler): def log_message(self, *args): pass @@ -184,9 +194,7 @@ def log_message(self, *args): @pytest.fixture(scope="session") def html_server(): """Serve the assets/ directory over HTTP; return base URL.""" - handler = lambda *args, **kwargs: _SilentHandler( - *args, directory=_ASSETS_DIR, **kwargs - ) + handler = lambda *args, **kwargs: _SilentHandler(*args, directory=_ASSETS_DIR, **kwargs) server = HTTPServer(("127.0.0.1", 0), handler) port = server.server_address[1] thread = threading.Thread(target=server.serve_forever, daemon=True) @@ -197,6 +205,7 @@ def html_server(): # ── Tauri / Electron helpers ────────────────────────────────────────────────── + def _download_and_extract(url: str, dest_dir: str, marker_path: str) -> None: """Download a zip and extract it; skip if marker_path already exists. @@ -229,6 +238,7 @@ def _download_binary(url: str, dest_path: str) -> None: # ── Tauri app ───────────────────────────────────────────────────────────────── + @pytest.fixture(scope="session") def tauri_app(): """Download, launch, and yield (proc, pid, base_url) for the Tauri test app. @@ -262,6 +272,7 @@ def tauri_app(): # ── Electron app ────────────────────────────────────────────────────────────── + @pytest.fixture(scope="session") def electron_app(html_server): """Download, launch, and yield (proc, pid, base_url) for the Electron test app. @@ -297,7 +308,8 @@ def electron_app(html_server): # Get the real pid of the launched app. out = subprocess.run( ["pgrep", "-f", "desktop-test-app-electron"], - capture_output=True, text=True, + capture_output=True, + text=True, ).stdout.strip() pid = int(out.split("\n")[0]) if out else 0 diff --git a/libs/cua-driver-rs/tests/integration/v2/harness/cv.py b/libs/cua-driver-rs/tests/integration/v2/harness/cv.py index d254ebfffe..d2bd34fa72 100644 --- a/libs/cua-driver-rs/tests/integration/v2/harness/cv.py +++ b/libs/cua-driver-rs/tests/integration/v2/harness/cv.py @@ -19,8 +19,9 @@ from typing import Optional, Tuple try: - from PIL import Image, ImageChops import numpy as np + from PIL import Image, ImageChops + _PIL_AVAILABLE = True except ImportError: _PIL_AVAILABLE = False @@ -90,7 +91,7 @@ def find_template( n_norm = (n_arr - n_mean) / n_std # Sliding-window NCC via FFT - from numpy.fft import fft2, ifft2, fftshift + from numpy.fft import fft2, fftshift, ifft2 # Zero-pad haystack to same size for FFT correlation pad_h = H + th - 1 @@ -102,12 +103,13 @@ def find_template( # Local haystack std for normalisation from numpy.lib.stride_tricks import sliding_window_view + windows = sliding_window_view(h_arr, (th, tw)) local_std = windows.reshape(-1, th * tw).std(axis=1).reshape(H - th + 1, W - tw + 1) local_std[local_std < 1e-6] = 1.0 # Trim correlation to valid region - corr_valid = corr[th - 1:th - 1 + (H - th + 1), tw - 1:tw - 1 + (W - tw + 1)] + corr_valid = corr[th - 1 : th - 1 + (H - th + 1), tw - 1 : tw - 1 + (W - tw + 1)] ncc = corr_valid / (local_std * th * tw) best_idx = np.argmax(ncc) diff --git a/libs/cua-driver-rs/tests/integration/v2/harness/driver.py b/libs/cua-driver-rs/tests/integration/v2/harness/driver.py index cdec18f487..c59bb866b1 100644 --- a/libs/cua-driver-rs/tests/integration/v2/harness/driver.py +++ b/libs/cua-driver-rs/tests/integration/v2/harness/driver.py @@ -29,7 +29,7 @@ _INTEG_DIR = os.path.dirname(_V2_DIR) sys.path.insert(0, _INTEG_DIR) -from driver_client import DriverClient, default_binary_path, MCPCallError # noqa: E402 +from driver_client import DriverClient, MCPCallError, default_binary_path # noqa: E402 @dataclass @@ -43,9 +43,10 @@ class WindowState: def find_element(self, label: str) -> Optional[int]: """Return the first element index whose line contains `label`.""" import re + for line in self.tree.split("\n"): if label in line: - m = re.search(r'\[(\d+)\]', line) + m = re.search(r"\[(\d+)\]", line) if m: return int(m.group(1)) return None @@ -53,12 +54,13 @@ def find_element(self, label: str) -> Optional[int]: def find_button(self, label: str) -> Optional[int]: """Return the first AXButton element index matching `label`.""" import re + for line in self.tree.split("\n"): if "AXButton" not in line: continue if label not in line: continue - m = re.search(r'\[(\d+)\]', line) + m = re.search(r"\[(\d+)\]", line) if m: return int(m.group(1)) return None @@ -66,12 +68,13 @@ def find_button(self, label: str) -> Optional[int]: def find_text_field(self, skip_url_bar: bool = True) -> Optional[int]: """Return the first AXTextField index (skipping Safari URL bar).""" import re + for line in self.tree.split("\n"): if "AXTextField" not in line: continue if skip_url_bar and "smart search field" in line: continue - m = re.search(r'\[(\d+)\]', line) + m = re.search(r"\[(\d+)\]", line) if m: return int(m.group(1)) return None @@ -79,6 +82,7 @@ def find_text_field(self, skip_url_bar: bool = True) -> Optional[int]: def ax_value(self, element_index: int) -> Optional[str]: """Return the value= attribute of the element at `element_index`.""" import re + prefix = f"[{element_index}]" for line in self.tree.split("\n"): if prefix not in line: @@ -138,8 +142,10 @@ def _area(w: dict) -> float: # Filter to on-screen windows with meaningful size. candidates = [ - w for w in windows - if w.get("is_on_screen") and w.get("on_current_space") is not False + w + for w in windows + if w.get("is_on_screen") + and w.get("on_current_space") is not False and _area(w) >= min_area ] if not candidates: @@ -234,14 +240,17 @@ def scroll( delta_x: float = 0.0, delta_y: float = -3.0, ) -> dict: - return self._c().call_tool("scroll", { - "pid": pid, - "window_id": window_id, - "x": x, - "y": y, - "delta_x": delta_x, - "delta_y": delta_y, - }) + return self._c().call_tool( + "scroll", + { + "pid": pid, + "window_id": window_id, + "x": x, + "y": y, + "delta_x": delta_x, + "delta_y": delta_y, + }, + ) def launch_app(self, bundle_id: str) -> dict: return self._c().call_tool("launch_app", {"bundle_id": bundle_id}) diff --git a/libs/cua-driver-rs/tests/integration/v2/harness/monitor.py b/libs/cua-driver-rs/tests/integration/v2/harness/monitor.py index c3469a5451..36e61501f7 100644 --- a/libs/cua-driver-rs/tests/integration/v2/harness/monitor.py +++ b/libs/cua-driver-rs/tests/integration/v2/harness/monitor.py @@ -22,19 +22,18 @@ from dataclasses import dataclass, field from typing import Optional - # Try to import PyObjC; fall back to no-op monitor on systems without it. try: - from AppKit import NSWorkspace - from AppKit import NSEvent + from AppKit import NSEvent, NSWorkspace from Quartz import ( CGWindowListCopyWindowInfo, - kCGWindowListOptionOnScreenOnly, kCGNullWindowID, kCGWindowLayer, - kCGWindowOwnerPID, + kCGWindowListOptionOnScreenOnly, kCGWindowName, + kCGWindowOwnerPID, ) + _PYOBJC_AVAILABLE = True except ImportError: _PYOBJC_AVAILABLE = False @@ -49,7 +48,7 @@ @dataclass class Violation: - kind: str # "cursor" | "frontmost" | "overlay_z" + kind: str # "cursor" | "frontmost" | "overlay_z" detail: str timestamp: float = field(default_factory=time.monotonic) @@ -189,9 +188,7 @@ def _check_frontmost(self) -> None: def _check_overlay_z(self) -> None: """Check that no cua-driver overlay window is above foreground windows.""" - wins = CGWindowListCopyWindowInfo( - kCGWindowListOptionOnScreenOnly, kCGNullWindowID - ) + wins = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID) if not wins: return @@ -204,8 +201,9 @@ def _check_overlay_z(self) -> None: name = str(w.get(kCGWindowName, "") or "") layer = int(w.get(kCGWindowLayer, 0)) - is_overlay = name in ("cua-agent-cursor", "cua-overlay") or \ - "cua-driver" in str(name).lower() + is_overlay = ( + name in ("cua-agent-cursor", "cua-overlay") or "cua-driver" in str(name).lower() + ) if is_overlay: overlay_layers.append(layer) diff --git a/libs/cua-driver-rs/tests/integration/v2/harness/tree.py b/libs/cua-driver-rs/tests/integration/v2/harness/tree.py index adc2c4fe34..b69f965c7d 100644 --- a/libs/cua-driver-rs/tests/integration/v2/harness/tree.py +++ b/libs/cua-driver-rs/tests/integration/v2/harness/tree.py @@ -24,7 +24,7 @@ def _lines(tree: str) -> list[tuple[int, str]]: """Return (element_index, raw_line) pairs for every indexed line.""" result: list[tuple[int, str]] = [] for line in tree.split("\n"): - m = re.search(r'\[(\d+)\]', line) + m = re.search(r"\[(\d+)\]", line) if m: result.append((int(m.group(1)), line)) return result diff --git a/libs/cua-driver-rs/tests/integration/v2/test_blender.py b/libs/cua-driver-rs/tests/integration/v2/test_blender.py index b0455e6055..64bf9aa72b 100644 --- a/libs/cua-driver-rs/tests/integration/v2/test_blender.py +++ b/libs/cua-driver-rs/tests/integration/v2/test_blender.py @@ -19,19 +19,19 @@ from __future__ import annotations +import json +import os import subprocess import sys import time -import os -import json import pytest _HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, _HERE) -from harness.driver import Driver from harness import tree as Tree -from harness.cv import decode, crop, diff_ratio, save_reference, load_reference +from harness.cv import crop, decode, diff_ratio, load_reference, save_reference +from harness.driver import Driver BLENDER_BUNDLE = "org.blenderfoundation.blender" _ASSETS_BLENDER = os.path.join(_HERE, "assets", "blender") @@ -53,6 +53,7 @@ def _save_bboxes(bboxes: dict) -> None: # ── module-level Blender setup ──────────────────────────────────────────────── + @pytest.fixture(scope="module") def blender_pid(binary, focus_monitor): """Launch Blender in background; return its pid.""" @@ -84,8 +85,11 @@ def blender_pid(binary, focus_monitor): def _reactivate_focus(focus_monitor): _, pid = focus_monitor subprocess.run( - ["osascript", "-e", - f'tell application "System Events" to set frontmost of (first process whose unix id is {pid}) to true'], + [ + "osascript", + "-e", + f'tell application "System Events" to set frontmost of (first process whose unix id is {pid}) to true', + ], check=False, ) time.sleep(0.4) @@ -93,6 +97,7 @@ def _reactivate_focus(focus_monitor): # ── tests ───────────────────────────────────────────────────────────────────── + class TestBlenderWindow: """Basic window state and AX tree tests.""" diff --git a/libs/cua-driver-rs/tests/integration/v2/test_chrome.py b/libs/cua-driver-rs/tests/integration/v2/test_chrome.py index 8cef3b3afd..db5229ed42 100644 --- a/libs/cua-driver-rs/tests/integration/v2/test_chrome.py +++ b/libs/cua-driver-rs/tests/integration/v2/test_chrome.py @@ -9,24 +9,25 @@ from __future__ import annotations +import os import re import subprocess import sys import time -import os import pytest _HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, _HERE) -from harness.driver import Driver from harness import tree as Tree +from harness.driver import Driver def _parse_clicks(tree_text: str) -> int: - m = re.search(r'clicks:\s*(\d+)', tree_text) + m = re.search(r"clicks:\s*(\d+)", tree_text) return int(m.group(1)) if m else 0 + CHROME_BUNDLE = "com.google.Chrome" FOCUS_MONITOR_BUNDLE = "com.trycua.FocusMonitorApp" @@ -70,8 +71,11 @@ def chrome_pid(binary, html_server, focus_monitor): # Hand focus to FocusMonitorApp by pid to avoid activating stale instances _, fm_pid = focus_monitor subprocess.run( - ["osascript", "-e", - f'tell application "System Events" to set frontmost of (first process whose unix id is {fm_pid}) to true'], + [ + "osascript", + "-e", + f'tell application "System Events" to set frontmost of (first process whose unix id is {fm_pid}) to true', + ], check=False, ) time.sleep(0.5) @@ -86,8 +90,11 @@ def chrome_pid(binary, html_server, focus_monitor): def _reactivate_focus(focus_monitor): _, pid = focus_monitor subprocess.run( - ["osascript", "-e", - f'tell application "System Events" to set frontmost of (first process whose unix id is {pid}) to true'], + [ + "osascript", + "-e", + f'tell application "System Events" to set frontmost of (first process whose unix id is {pid}) to true', + ], check=False, ) time.sleep(0.4) @@ -95,13 +102,16 @@ def _reactivate_focus(focus_monitor): # ── tests ───────────────────────────────────────────────────────────────────── + class TestChromeButton: def test_click_increments_counter(self, driver, chrome_pid, ux_guard): wid = driver.find_window(chrome_pid) full = driver.get_window_state(chrome_pid, wid) idx = full.find_element("Click Me") - assert idx is not None, f"'Click Me' not found in tree (first 800 chars):\n{full.tree[:800]}" + assert ( + idx is not None + ), f"'Click Me' not found in tree (first 800 chars):\n{full.tree[:800]}" count_before = _parse_clicks(full.tree) driver.click(chrome_pid, wid, element_index=idx) @@ -110,7 +120,9 @@ def test_click_increments_counter(self, driver, chrome_pid, ux_guard): after = driver.get_window_state(chrome_pid, wid) count_after = _parse_clicks(after.tree) print(f"\n click counter: {count_before} → {count_after}") - assert count_after == count_before + 1, f"Counter did not increment: {count_before} → {count_after}" + assert ( + count_after == count_before + 1 + ), f"Counter did not increment: {count_before} → {count_after}" def test_click_increments_twice(self, driver, chrome_pid, ux_guard): wid = driver.find_window(chrome_pid) @@ -127,7 +139,9 @@ def test_click_increments_twice(self, driver, chrome_pid, ux_guard): after = driver.get_window_state(chrome_pid, wid) count_after = _parse_clicks(after.tree) print(f"\n click counter: {count_before} → {count_after}") - assert count_after == count_before + 2, f"Counter should have incremented by 2: {count_before} → {count_after}" + assert ( + count_after == count_before + 2 + ), f"Counter should have incremented by 2: {count_before} → {count_after}" class TestChromeTextInput: @@ -160,6 +174,7 @@ def test_canvas_pixel_click_no_focus_steal(self, driver, chrome_pid, ux_guard): assert mid.screenshot_b64, "No screenshot" from harness.cv import decode, diff_ratio + before_img = decode(mid.screenshot_b64) x = mid.screenshot_width // 2 diff --git a/libs/cua-driver-rs/tests/integration/v2/test_electron.py b/libs/cua-driver-rs/tests/integration/v2/test_electron.py index 5c9818ceca..777ad4eaa4 100644 --- a/libs/cua-driver-rs/tests/integration/v2/test_electron.py +++ b/libs/cua-driver-rs/tests/integration/v2/test_electron.py @@ -11,23 +11,24 @@ from __future__ import annotations +import os import subprocess import sys import time -import os import pytest try: import requests as _requests + _REQUESTS_AVAILABLE = True except ImportError: _REQUESTS_AVAILABLE = False _HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, _HERE) -from harness.driver import Driver from harness import tree as Tree +from harness.driver import Driver _ELECTRON_API = "http://localhost:6769" @@ -55,8 +56,11 @@ def _http_reset(base_url: str = _ELECTRON_API) -> None: def _reactivate_focus(focus_monitor): _, pid = focus_monitor subprocess.run( - ["osascript", "-e", - f'tell application "System Events" to set frontmost of (first process whose unix id is {pid}) to true'], + [ + "osascript", + "-e", + f'tell application "System Events" to set frontmost of (first process whose unix id is {pid}) to true', + ], check=False, ) time.sleep(0.4) @@ -64,6 +68,7 @@ def _reactivate_focus(focus_monitor): # ── tests ───────────────────────────────────────────────────────────────────── + class TestElectronButton: def test_click_increments_counter(self, driver, electron_app, ux_guard): _, pid, base_url = electron_app @@ -103,11 +108,14 @@ def test_type_text(self, driver, electron_app, ux_guard): try: driver.click(pid, wid, element_index=idx) time.sleep(0.4) - driver.call_tool("type_text_chars", { - "pid": pid, - "text": "hello electron", - "delay_ms": 30, - }) + driver.call_tool( + "type_text_chars", + { + "pid": pid, + "text": "hello electron", + "delay_ms": 30, + }, + ) except Exception as e: events = _http_events(base_url) key_events = [ev for ev in events if ev.get("type") in ("keydown", "keypress", "input")] @@ -135,7 +143,9 @@ def test_type_text(self, driver, electron_app, ux_guard): if after is None or not after.tree: pytest.skip("AX tree unavailable after typing (Electron AX rebuild timeout)") - print(f"\n after tree length: {len(after.tree)}, has text: {after.has_text('hello electron')}") + print( + f"\n after tree length: {len(after.tree)}, has text: {after.has_text('hello electron')}" + ) assert after.has_text("hello electron"), f"Text not in AX tree:\n{after.tree[:500]}" @@ -162,6 +172,7 @@ def test_canvas_pixel_click_no_focus_steal(self, driver, electron_app, ux_guard) assert mid.screenshot_b64, "No screenshot after 5 retries" from harness.cv import decode, diff_ratio + before_img = decode(mid.screenshot_b64) x = mid.screenshot_width // 2 diff --git a/libs/cua-driver-rs/tests/integration/v2/test_safari.py b/libs/cua-driver-rs/tests/integration/v2/test_safari.py index 5a33846482..a2e39becab 100644 --- a/libs/cua-driver-rs/tests/integration/v2/test_safari.py +++ b/libs/cua-driver-rs/tests/integration/v2/test_safari.py @@ -13,31 +13,33 @@ from __future__ import annotations +import os +import re import subprocess import sys import time -import os -import re import pytest _HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, _HERE) -from harness.driver import Driver from harness import tree as Tree +from harness.driver import Driver def _parse_clicks(tree_text: str) -> int: """Extract the 'clicks: N' counter value from the AX tree.""" - m = re.search(r'clicks:\s*(\d+)', tree_text) + m = re.search(r"clicks:\s*(\d+)", tree_text) return int(m.group(1)) if m else 0 + SAFARI_BUNDLE = "com.apple.Safari" FOCUS_MONITOR_BUNDLE = "com.trycua.FocusMonitorApp" # ── module-level Safari setup ───────────────────────────────────────────────── + @pytest.fixture(scope="module") def safari_pid(binary, html_server, focus_monitor): """Open Safari to test_page.html; return its pid. @@ -68,8 +70,11 @@ def _reactivate_focus(focus_monitor): """Restore FocusMonitorApp as frontmost before each test (by pid).""" _, pid = focus_monitor subprocess.run( - ["osascript", "-e", - f'tell application "System Events" to set frontmost of (first process whose unix id is {pid}) to true'], + [ + "osascript", + "-e", + f'tell application "System Events" to set frontmost of (first process whose unix id is {pid}) to true', + ], check=False, ) time.sleep(0.4) @@ -77,6 +82,7 @@ def _reactivate_focus(focus_monitor): # ── helpers ─────────────────────────────────────────────────────────────────── + def _get_state(d: Driver, pid: int, safari_pid_fixture) -> "harness.driver.WindowState": wid = d.find_window(pid) return d.get_window_state(pid, wid) @@ -84,6 +90,7 @@ def _get_state(d: Driver, pid: int, safari_pid_fixture) -> "harness.driver.Windo # ── tests ───────────────────────────────────────────────────────────────────── + class TestSafariButton: """AX-click the 'Click Me' button without stealing focus.""" @@ -102,9 +109,9 @@ def test_click_increments_counter(self, driver, safari_pid, ux_guard): after = driver.get_window_state(safari_pid, wid) count_after = _parse_clicks(after.tree) print(f"\n click counter: {count_before} → {count_after}") - assert count_after == count_before + 1, ( - f"Counter did not increment: {count_before} → {count_after}" - ) + assert ( + count_after == count_before + 1 + ), f"Counter did not increment: {count_before} → {count_after}" def test_click_increments_twice(self, driver, safari_pid, ux_guard): wid = driver.find_window(safari_pid) @@ -123,9 +130,9 @@ def test_click_increments_twice(self, driver, safari_pid, ux_guard): after = driver.get_window_state(safari_pid, wid) count_after = _parse_clicks(after.tree) print(f"\n click counter: {count_before} → {count_after}") - assert count_after == count_before + 2, ( - f"Counter should have incremented by 2: {count_before} → {count_after}" - ) + assert ( + count_after == count_before + 2 + ), f"Counter should have incremented by 2: {count_before} → {count_after}" class TestSafariTextInput: @@ -150,9 +157,7 @@ def test_type_text_appears_in_ax_tree(self, driver, safari_pid, ux_guard): time.sleep(0.6) after = driver.get_window_state(safari_pid, wid) - assert after.has_text("hello safari"), ( - f"Typed text not visible in AX tree:\n{after.tree}" - ) + assert after.has_text("hello safari"), f"Typed text not visible in AX tree:\n{after.tree}" def test_type_text_with_type_text_tool(self, driver, safari_pid, ux_guard): """Use type_text (not type_text_chars) and verify result.""" @@ -172,9 +177,7 @@ def test_type_text_with_type_text_tool(self, driver, safari_pid, ux_guard): time.sleep(0.6) after = driver.get_window_state(safari_pid, wid) - assert after.has_text("type_text test"), ( - f"type_text result not in AX tree:\n{after.tree}" - ) + assert after.has_text("type_text test"), f"type_text result not in AX tree:\n{after.tree}" class TestSafariCheckbox: @@ -194,9 +197,7 @@ def test_checkbox_toggle(self, driver, safari_pid, ux_guard): time.sleep(0.4) after = driver.get_window_state(safari_pid, wid) - assert after.has_text("checked"), ( - f"Checkbox did not toggle to checked:\n{after.tree}" - ) + assert after.has_text("checked"), f"Checkbox did not toggle to checked:\n{after.tree}" class TestSafariSelect: @@ -223,16 +224,21 @@ def test_select_option_b(self, driver, safari_pid, ux_guard): driver.click(safari_pid, wid, element_index=opt_b) else: # Fallback: set value directly - driver.call_tool("set_value", { - "pid": safari_pid, "window_id": wid, - "element_index": idx, "value": "b", - }) + driver.call_tool( + "set_value", + { + "pid": safari_pid, + "window_id": wid, + "element_index": idx, + "value": "b", + }, + ) time.sleep(0.4) after = driver.get_window_state(safari_pid, wid) - assert after.has_text("Option B") or after.has_text("selected: Option B"), ( - f"Dropdown did not select Option B:\n{after.tree}" - ) + assert after.has_text("Option B") or after.has_text( + "selected: Option B" + ), f"Dropdown did not select Option B:\n{after.tree}" class TestSafariTextarea: @@ -262,19 +268,22 @@ def test_textarea_input(self, driver, safari_pid, ux_guard): time.sleep(0.8) # longer settle for WKWebView textarea focus # Step 3: type — keystrokes go to the focused element - driver.call_tool("type_text_chars", { - "pid": safari_pid, - "text": "multi line", - "delay_ms": 50, - }) + driver.call_tool( + "type_text_chars", + { + "pid": safari_pid, + "text": "multi line", + "delay_ms": 50, + }, + ) time.sleep(0.6) after = driver.get_window_state(safari_pid, wid) val = Tree.ax_value(after.tree, ta_idx) print(f"\n textarea AX value: {val!r}") - assert after.has_text("multi line"), ( - f"Textarea text not in AX tree; val={val!r};\n{after.tree[:800]}" - ) + assert after.has_text( + "multi line" + ), f"Textarea text not in AX tree; val={val!r};\n{after.tree[:800]}" class TestSafariCanvas: @@ -295,6 +304,7 @@ def test_canvas_pixel_click_no_focus_steal(self, driver, safari_pid, ux_guard): print(f"\n screenshot: {before.screenshot_width}x{before.screenshot_height}") from harness.cv import decode, diff_ratio + before_img = decode(before.screenshot_b64) # Canvas is the last section on the page. With ~80px browser chrome diff --git a/libs/cua-driver-rs/tests/integration/v2/test_tauri.py b/libs/cua-driver-rs/tests/integration/v2/test_tauri.py index e1bb465f56..ed233bdc7c 100644 --- a/libs/cua-driver-rs/tests/integration/v2/test_tauri.py +++ b/libs/cua-driver-rs/tests/integration/v2/test_tauri.py @@ -11,23 +11,24 @@ from __future__ import annotations +import os import subprocess import sys import time -import os import pytest try: import requests as _requests + _REQUESTS_AVAILABLE = True except ImportError: _REQUESTS_AVAILABLE = False _HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, _HERE) -from harness.driver import Driver from harness import tree as Tree +from harness.driver import Driver _TAURI_API = "http://localhost:6769" @@ -55,8 +56,11 @@ def _http_reset(base_url: str = _TAURI_API) -> None: def _reactivate_focus(focus_monitor): _, pid = focus_monitor subprocess.run( - ["osascript", "-e", - f'tell application "System Events" to set frontmost of (first process whose unix id is {pid}) to true'], + [ + "osascript", + "-e", + f'tell application "System Events" to set frontmost of (first process whose unix id is {pid}) to true', + ], check=False, ) time.sleep(0.4) @@ -64,6 +68,7 @@ def _reactivate_focus(focus_monitor): # ── tests ───────────────────────────────────────────────────────────────────── + class TestTauriButton: def test_button_click(self, driver, tauri_app, ux_guard): _, pid, base_url = tauri_app @@ -128,9 +133,9 @@ def test_type_text(self, driver, tauri_app, ux_guard): key_events = [e for e in events if e.get("type") in ("key_down", "keydown", "input")] print(f" key events: {len(key_events)}") - assert text_in_tree or len(key_events) >= len("tauri test"), ( - f"Text not found in AX tree and insufficient key events:\n{after.tree}" - ) + assert text_in_tree or len(key_events) >= len( + "tauri test" + ), f"Text not found in AX tree and insufficient key events:\n{after.tree}" class TestTauriNoFocusSteal: diff --git a/libs/cua-driver/Tests/integration/conftest.py b/libs/cua-driver/Tests/integration/conftest.py index e2c21409d6..ddfa5194f5 100644 --- a/libs/cua-driver/Tests/integration/conftest.py +++ b/libs/cua-driver/Tests/integration/conftest.py @@ -16,16 +16,16 @@ import os import subprocess import sys -import time import threading -from http.server import HTTPServer, SimpleHTTPRequestHandler +import time import urllib.request +from http.server import HTTPServer, SimpleHTTPRequestHandler import pytest _HERE = os.path.dirname(os.path.abspath(__file__)) -_TESTS_DIR = os.path.dirname(_HERE) # Tests/ -_DRIVER_ROOT = os.path.dirname(_TESTS_DIR) # libs/cua-driver/ +_TESTS_DIR = os.path.dirname(_HERE) # Tests/ +_DRIVER_ROOT = os.path.dirname(_TESTS_DIR) # libs/cua-driver/ sys.path.insert(0, _HERE) sys.path.insert(0, os.path.join(_HERE, "harness")) @@ -59,6 +59,7 @@ # ── binary ──────────────────────────────────────────────────────────────────── + @pytest.fixture(scope="session") def binary() -> str: return default_binary_path() @@ -66,16 +67,19 @@ def binary() -> str: # ── driver ──────────────────────────────────────────────────────────────────── + @pytest.fixture def driver(binary): """Yield a started Driver instance.""" from harness.driver import Driver + with Driver(binary) as d: yield d # ── focus monitor ───────────────────────────────────────────────────────────── + def _build_focus_app() -> None: if not os.path.exists(_FOCUS_APP_EXE): build_sh = os.path.join(_FOCUS_APP_DIR, "build.sh") @@ -142,9 +146,12 @@ def activate_focus_monitor(focus_monitor): """Re-activate FocusMonitorApp before each test (by pid).""" _, pid = focus_monitor subprocess.run( - ["osascript", "-e", - f'tell application "System Events" to set frontmost of ' - f'(first process whose unix id is {pid}) to true'], + [ + "osascript", + "-e", + f'tell application "System Events" to set frontmost of ' + f"(first process whose unix id is {pid}) to true", + ], check=False, ) time.sleep(0.4) @@ -152,11 +159,13 @@ def activate_focus_monitor(focus_monitor): # ── UX guard ────────────────────────────────────────────────────────────────── + @pytest.fixture def ux_guard(focus_monitor): """Start UXMonitor before the test, call assert_clean() after.""" _, sentinel_pid = focus_monitor from harness.monitor import UXMonitor + mon = UXMonitor(sentinel_pid=sentinel_pid) mon.start() yield mon @@ -166,6 +175,7 @@ def ux_guard(focus_monitor): # ── local HTML server ───────────────────────────────────────────────────────── + class _SilentHandler(SimpleHTTPRequestHandler): def log_message(self, *args): pass @@ -174,9 +184,7 @@ def log_message(self, *args): @pytest.fixture(scope="session") def html_server(): """Serve the assets/ directory over HTTP; return base URL.""" - handler = lambda *args, **kwargs: _SilentHandler( - *args, directory=_ASSETS_DIR, **kwargs - ) + handler = lambda *args, **kwargs: _SilentHandler(*args, directory=_ASSETS_DIR, **kwargs) server = HTTPServer(("127.0.0.1", 0), handler) port = server.server_address[1] thread = threading.Thread(target=server.serve_forever, daemon=True) @@ -187,6 +195,7 @@ def html_server(): # ── download helpers ────────────────────────────────────────────────────────── + def _download_and_extract(url: str, dest_dir: str, marker_path: str) -> None: if os.path.exists(marker_path): return @@ -210,6 +219,7 @@ def _download_binary(url: str, dest_path: str) -> None: # ── Tauri app ───────────────────────────────────────────────────────────────── + @pytest.fixture(scope="session") def tauri_app(): """Download, launch, and yield (proc, pid, base_url) for the Tauri test app.""" @@ -239,6 +249,7 @@ def tauri_app(): # ── Electron app ────────────────────────────────────────────────────────────── + @pytest.fixture(scope="session") def electron_app(html_server): """Download, launch, and yield (proc, pid, base_url) for the Electron test app.""" @@ -267,7 +278,8 @@ def electron_app(html_server): out = subprocess.run( ["pgrep", "-f", "desktop-test-app-electron"], - capture_output=True, text=True, + capture_output=True, + text=True, ).stdout.strip() pid = int(out.split("\n")[0]) if out else 0 diff --git a/libs/cua-driver/Tests/integration/driver_client.py b/libs/cua-driver/Tests/integration/driver_client.py index 46728545d8..141975cc25 100644 --- a/libs/cua-driver/Tests/integration/driver_client.py +++ b/libs/cua-driver/Tests/integration/driver_client.py @@ -79,9 +79,7 @@ def _notify(self, method: str, params: Optional[dict] = None) -> None: payload["params"] = params self._write(payload) - def _call( - self, method: str, params: Optional[dict] = None, timeout: float = 20.0 - ) -> dict: + def _call(self, method: str, params: Optional[dict] = None, timeout: float = 20.0) -> dict: self._next_id += 1 request_id = self._next_id payload = {"jsonrpc": "2.0", "id": request_id, "method": method} @@ -191,8 +189,7 @@ def resolve_window_id( if require_on_current_space: preferred = [ - w for w in windows - if w.get("is_on_screen") and w.get("on_current_space") is not False + w for w in windows if w.get("is_on_screen") and w.get("on_current_space") is not False ] if preferred: preferred.sort(key=lambda w: w.get("z_index", 0), reverse=True) diff --git a/libs/cua-driver/Tests/integration/harness/cv.py b/libs/cua-driver/Tests/integration/harness/cv.py index d254ebfffe..d2bd34fa72 100644 --- a/libs/cua-driver/Tests/integration/harness/cv.py +++ b/libs/cua-driver/Tests/integration/harness/cv.py @@ -19,8 +19,9 @@ from typing import Optional, Tuple try: - from PIL import Image, ImageChops import numpy as np + from PIL import Image, ImageChops + _PIL_AVAILABLE = True except ImportError: _PIL_AVAILABLE = False @@ -90,7 +91,7 @@ def find_template( n_norm = (n_arr - n_mean) / n_std # Sliding-window NCC via FFT - from numpy.fft import fft2, ifft2, fftshift + from numpy.fft import fft2, fftshift, ifft2 # Zero-pad haystack to same size for FFT correlation pad_h = H + th - 1 @@ -102,12 +103,13 @@ def find_template( # Local haystack std for normalisation from numpy.lib.stride_tricks import sliding_window_view + windows = sliding_window_view(h_arr, (th, tw)) local_std = windows.reshape(-1, th * tw).std(axis=1).reshape(H - th + 1, W - tw + 1) local_std[local_std < 1e-6] = 1.0 # Trim correlation to valid region - corr_valid = corr[th - 1:th - 1 + (H - th + 1), tw - 1:tw - 1 + (W - tw + 1)] + corr_valid = corr[th - 1 : th - 1 + (H - th + 1), tw - 1 : tw - 1 + (W - tw + 1)] ncc = corr_valid / (local_std * th * tw) best_idx = np.argmax(ncc) diff --git a/libs/cua-driver/Tests/integration/harness/driver.py b/libs/cua-driver/Tests/integration/harness/driver.py index f3d555ed13..fa4be89739 100644 --- a/libs/cua-driver/Tests/integration/harness/driver.py +++ b/libs/cua-driver/Tests/integration/harness/driver.py @@ -28,7 +28,7 @@ _INTEG_DIR = os.path.dirname(_HERE) sys.path.insert(0, _INTEG_DIR) -from driver_client import DriverClient, default_binary_path, MCPCallError # noqa: E402 +from driver_client import DriverClient, MCPCallError, default_binary_path # noqa: E402 @dataclass @@ -42,9 +42,10 @@ class WindowState: def find_element(self, label: str) -> Optional[int]: """Return the first element index whose line contains `label`.""" import re + for line in self.tree.split("\n"): if label in line: - m = re.search(r'\[(\d+)\]', line) + m = re.search(r"\[(\d+)\]", line) if m: return int(m.group(1)) return None @@ -52,12 +53,13 @@ def find_element(self, label: str) -> Optional[int]: def find_button(self, label: str) -> Optional[int]: """Return the first AXButton element index matching `label`.""" import re + for line in self.tree.split("\n"): if "AXButton" not in line: continue if label not in line: continue - m = re.search(r'\[(\d+)\]', line) + m = re.search(r"\[(\d+)\]", line) if m: return int(m.group(1)) return None @@ -65,13 +67,14 @@ def find_button(self, label: str) -> Optional[int]: def find_text_field(self, skip_url_bar: bool = True) -> Optional[int]: """Return the first AXTextField index (skipping browser URL bars).""" import re + _URL_BAR_HINTS = ("smart search field", "address and search bar", "location") for line in self.tree.split("\n"): if "AXTextField" not in line: continue if skip_url_bar and any(h in line.lower() for h in _URL_BAR_HINTS): continue - m = re.search(r'\[(\d+)\]', line) + m = re.search(r"\[(\d+)\]", line) if m: return int(m.group(1)) return None @@ -79,6 +82,7 @@ def find_text_field(self, skip_url_bar: bool = True) -> Optional[int]: def ax_value(self, element_index: int) -> Optional[str]: """Return the value= attribute of the element at `element_index`.""" import re + prefix = f"[{element_index}]" for line in self.tree.split("\n"): if prefix not in line: @@ -143,8 +147,10 @@ def _area(w: dict) -> float: # Filter to on-screen windows with meaningful size. candidates = [ - w for w in windows - if w.get("is_on_screen") and w.get("on_current_space") is not False + w + for w in windows + if w.get("is_on_screen") + and w.get("on_current_space") is not False and _area(w) >= min_area ] if not candidates: @@ -161,9 +167,7 @@ def _area(w: dict) -> float: # Probe each candidate for AX tree content; return the first one that # has a non-empty tree. This is needed for Chrome, whose AX tree is # attached to toolbar/helper windows rather than the large content window. - probe_order = [primary_wid] + [ - w["window_id"] for w in candidates[1:] - ] + probe_order = [primary_wid] + [w["window_id"] for w in candidates[1:]] # Also include off-screen windows as a last resort (Chrome toolbar # windows are often flagged off-screen even while Chrome is running). for w in sorted(windows, key=_area, reverse=True): @@ -204,7 +208,8 @@ def get_window_state( # Format: "
\n\n" where the tree starts with "- AX..." if not tree and text_content: import re - m = re.search(r'\n\n(- .+)', text_content, re.DOTALL) + + m = re.search(r"\n\n(- .+)", text_content, re.DOTALL) if m: tree = m.group(1) width = sc.get("screenshot_width", 0) @@ -272,14 +277,17 @@ def scroll( delta_x: float = 0.0, delta_y: float = -3.0, ) -> dict: - return self._c().call_tool("scroll", { - "pid": pid, - "window_id": window_id, - "x": x, - "y": y, - "delta_x": delta_x, - "delta_y": delta_y, - }) + return self._c().call_tool( + "scroll", + { + "pid": pid, + "window_id": window_id, + "x": x, + "y": y, + "delta_x": delta_x, + "delta_y": delta_y, + }, + ) def launch_app(self, bundle_id: str) -> dict: return self._c().call_tool("launch_app", {"bundle_id": bundle_id}) diff --git a/libs/cua-driver/Tests/integration/harness/monitor.py b/libs/cua-driver/Tests/integration/harness/monitor.py index c3469a5451..36e61501f7 100644 --- a/libs/cua-driver/Tests/integration/harness/monitor.py +++ b/libs/cua-driver/Tests/integration/harness/monitor.py @@ -22,19 +22,18 @@ from dataclasses import dataclass, field from typing import Optional - # Try to import PyObjC; fall back to no-op monitor on systems without it. try: - from AppKit import NSWorkspace - from AppKit import NSEvent + from AppKit import NSEvent, NSWorkspace from Quartz import ( CGWindowListCopyWindowInfo, - kCGWindowListOptionOnScreenOnly, kCGNullWindowID, kCGWindowLayer, - kCGWindowOwnerPID, + kCGWindowListOptionOnScreenOnly, kCGWindowName, + kCGWindowOwnerPID, ) + _PYOBJC_AVAILABLE = True except ImportError: _PYOBJC_AVAILABLE = False @@ -49,7 +48,7 @@ @dataclass class Violation: - kind: str # "cursor" | "frontmost" | "overlay_z" + kind: str # "cursor" | "frontmost" | "overlay_z" detail: str timestamp: float = field(default_factory=time.monotonic) @@ -189,9 +188,7 @@ def _check_frontmost(self) -> None: def _check_overlay_z(self) -> None: """Check that no cua-driver overlay window is above foreground windows.""" - wins = CGWindowListCopyWindowInfo( - kCGWindowListOptionOnScreenOnly, kCGNullWindowID - ) + wins = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID) if not wins: return @@ -204,8 +201,9 @@ def _check_overlay_z(self) -> None: name = str(w.get(kCGWindowName, "") or "") layer = int(w.get(kCGWindowLayer, 0)) - is_overlay = name in ("cua-agent-cursor", "cua-overlay") or \ - "cua-driver" in str(name).lower() + is_overlay = ( + name in ("cua-agent-cursor", "cua-overlay") or "cua-driver" in str(name).lower() + ) if is_overlay: overlay_layers.append(layer) diff --git a/libs/cua-driver/Tests/integration/harness/tree.py b/libs/cua-driver/Tests/integration/harness/tree.py index adc2c4fe34..b69f965c7d 100644 --- a/libs/cua-driver/Tests/integration/harness/tree.py +++ b/libs/cua-driver/Tests/integration/harness/tree.py @@ -24,7 +24,7 @@ def _lines(tree: str) -> list[tuple[int, str]]: """Return (element_index, raw_line) pairs for every indexed line.""" result: list[tuple[int, str]] = [] for line in tree.split("\n"): - m = re.search(r'\[(\d+)\]', line) + m = re.search(r"\[(\d+)\]", line) if m: result.append((int(m.group(1)), line)) return result diff --git a/libs/cua-driver/Tests/integration/test_background_focus.py b/libs/cua-driver/Tests/integration/test_background_focus.py index dd1674411b..c010400cd3 100644 --- a/libs/cua-driver/Tests/integration/test_background_focus.py +++ b/libs/cua-driver/Tests/integration/test_background_focus.py @@ -39,9 +39,7 @@ _HTML_PAGE = os.path.join(_THIS_DIR, "fixtures", "interactive.html") _FOCUS_APP_DIR = os.path.join(_REPO_ROOT, "Tests", "FocusMonitorApp") _FOCUS_APP_BUNDLE = os.path.join(_FOCUS_APP_DIR, "FocusMonitorApp.app") -_FOCUS_APP_EXE = os.path.join( - _FOCUS_APP_BUNDLE, "Contents", "MacOS", "FocusMonitorApp" -) +_FOCUS_APP_EXE = os.path.join(_FOCUS_APP_BUNDLE, "Contents", "MacOS", "FocusMonitorApp") _LOSS_FILE = "/tmp/focus_monitor_losses.txt" SAFARI_BUNDLE = "com.apple.Safari" @@ -52,6 +50,7 @@ # Helpers # --------------------------------------------------------------------------- + def _tool_text(result: dict) -> str: """Extract text from a tool call result's content array.""" for item in result.get("content", []): @@ -59,11 +58,10 @@ def _tool_text(result: dict) -> str: return item.get("text", "") return "" + def _build_focus_app() -> None: if not os.path.exists(_FOCUS_APP_EXE): - subprocess.run( - [os.path.join(_FOCUS_APP_DIR, "build.sh")], check=True - ) + subprocess.run([os.path.join(_FOCUS_APP_DIR, "build.sh")], check=True) def _launch_focus_app() -> tuple[subprocess.Popen, int]: @@ -100,12 +98,15 @@ def _open_safari_to_html(client: DriverClient) -> int: document load back to whatever was frontmost before the call. """ file_url = f"file://{_HTML_PAGE}" - result = client.call_tool("launch_app", { - "bundle_id": SAFARI_BUNDLE, - "urls": [file_url], - }) + result = client.call_tool( + "launch_app", + { + "bundle_id": SAFARI_BUNDLE, + "urls": [file_url], + }, + ) text = _tool_text(result) - m = re.search(r'pid[=:\s]+(\d+)', text, re.IGNORECASE) + m = re.search(r"pid[=:\s]+(\d+)", text, re.IGNORECASE) if not m: raise RuntimeError(f"launch_app did not return a pid: {text[:200]}") time.sleep(2.0) # let page load @@ -115,9 +116,7 @@ def _open_safari_to_html(client: DriverClient) -> int: def _get_page_text(client: DriverClient, pid: int) -> str: """Return the AX tree markdown for Safari.""" window_id = resolve_window_id(client, pid) - result = client.call_tool( - "get_window_state", {"pid": pid, "window_id": window_id} - ) + result = client.call_tool("get_window_state", {"pid": pid, "window_id": window_id}) return result.get("structuredContent", result).get("tree_markdown", "") @@ -125,7 +124,7 @@ def _find_element_index(tree_markdown: str, label: str) -> int | None: """Extract the first [N] element index from a line containing `label`.""" for line in tree_markdown.split("\n"): if label in line: - m = re.search(r'\[(\d+)\]', line) + m = re.search(r"\[(\d+)\]", line) if m: return int(m.group(1)) return None @@ -143,25 +142,25 @@ def _find_calc_button(tree_markdown: str, label: str) -> int | None: for line in tree_markdown.split("\n"): if "AXButton" not in line: continue - m = re.search(r'\[(\d+)\]', line) + m = re.search(r"\[(\d+)\]", line) if not m: continue idx = int(m.group(1)) # Match (Label) pattern — exact match in parens - if f'({label})' in line: + if f"({label})" in line: return idx # Also match help text if f'help="{label}' in line: return idx # Also match id=Label - if f'id={label}' in line: + if f"id={label}" in line: return idx return None def _extract_click_count(tree_markdown: str) -> int | None: """Extract the number from 'clicks: N' in the AX tree.""" - m = re.search(r'clicks:\s*(\d+)', tree_markdown) + m = re.search(r"clicks:\s*(\d+)", tree_markdown) return int(m.group(1)) if m else None @@ -178,6 +177,7 @@ def _activate_focus_monitor() -> None: # Test class # --------------------------------------------------------------------------- + class BackgroundFocusTests(unittest.TestCase): """Click & type into backgrounded Safari without stealing focus.""" @@ -210,9 +210,9 @@ def setUpClass(cls) -> None: # Confirm FocusMonitorApp is frontmost. with DriverClient(cls.binary) as c: active = frontmost_bundle_id(c) - assert active == FOCUS_MONITOR_BUNDLE, ( - f"Expected FocusMonitorApp frontmost, got {active}" - ) + assert ( + active == FOCUS_MONITOR_BUNDLE + ), f"Expected FocusMonitorApp frontmost, got {active}" # Baseline: 0 focus losses. losses = _read_focus_losses() @@ -229,7 +229,8 @@ def tearDownClass(cls) -> None: # Close the Safari tab we opened (best-effort) subprocess.run( [ - "osascript", "-e", + "osascript", + "-e", 'tell application "Safari" to close (every tab of window 1 ' 'whose URL contains "interactive.html")', ], @@ -252,7 +253,8 @@ def setUp(self) -> None: with DriverClient(self.binary) as c: active = frontmost_bundle_id(c) self.assertEqual( - active, FOCUS_MONITOR_BUNDLE, + active, + FOCUS_MONITOR_BUNDLE, f"FocusMonitorApp not frontmost at test start, got {active}", ) @@ -267,15 +269,17 @@ def _assert_no_focus_loss(self, label: str) -> None: losses = _read_focus_losses() with DriverClient(self.binary) as c: active = frontmost_bundle_id(c) - focus_restored = (active == FOCUS_MONITOR_BUNDLE) + focus_restored = active == FOCUS_MONITOR_BUNDLE loss_delta = losses - self._losses_before - print(f" [{label}] losses: {self._losses_before}->{losses} " - f"(delta={loss_delta}), frontmost: {active}, " - f"restored: {focus_restored}") + print( + f" [{label}] losses: {self._losses_before}->{losses} " + f"(delta={loss_delta}), frontmost: {active}, " + f"restored: {focus_restored}" + ) self.assertEqual( - active, FOCUS_MONITOR_BUNDLE, - f"[{label}] Focus not restored — " - f"frontmost is {active}, not FocusMonitorApp", + active, + FOCUS_MONITOR_BUNDLE, + f"[{label}] Focus not restored — " f"frontmost is {active}, not FocusMonitorApp", ) # -- AX element_index click (pure AX action, no cursor move) ----------- @@ -284,22 +288,28 @@ def test_01_ax_click_button(self) -> None: """AX-click the 'Click Me' button in backgrounded Safari.""" with DriverClient(self.binary) as c: window_id = resolve_window_id(c, self._safari_pid) - snap = c.call_tool("get_window_state", { - "pid": self._safari_pid, - "window_id": window_id, - "query": "Click Me", - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._safari_pid, + "window_id": window_id, + "query": "Click Me", + }, + ) tree = snap.get("structuredContent", snap).get("tree_markdown", "") print(f"\n filtered tree:\n{tree}") idx = _find_element_index(tree, "Click Me") self.assertIsNotNone(idx, "Could not find 'Click Me' button in AX tree") - result = c.call_tool("click", { - "pid": self._safari_pid, - "window_id": window_id, - "element_index": idx, - }) + result = c.call_tool( + "click", + { + "pid": self._safari_pid, + "window_id": window_id, + "element_index": idx, + }, + ) print(f" click result: {result}") time.sleep(0.5) @@ -322,11 +332,14 @@ def test_02_type_text_chars(self) -> None: """ with DriverClient(self.binary) as c: window_id = resolve_window_id(c, self._safari_pid) - snap = c.call_tool("get_window_state", { - "pid": self._safari_pid, - "window_id": window_id, - "query": "AXTextField", - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._safari_pid, + "window_id": window_id, + "query": "AXTextField", + }, + ) tree = snap.get("structuredContent", snap).get("tree_markdown", "") print(f"\n filtered tree:\n{tree}") @@ -334,19 +347,23 @@ def test_02_type_text_chars(self) -> None: idx = None for line in tree.split("\n"): if "AXTextField" in line and "smart search field" not in line: - m = re.search(r'\[(\d+)\]', line) + m = re.search(r"\[(\d+)\]", line) if m: idx = int(m.group(1)) break if idx is None: - snap = c.call_tool("get_window_state", { - "pid": self._safari_pid, "window_id": window_id, - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._safari_pid, + "window_id": window_id, + }, + ) tree = snap.get("structuredContent", snap).get("tree_markdown", "") for line in tree.split("\n"): if "AXTextField" in line and "smart search field" not in line: - m = re.search(r'\[(\d+)\]', line) + m = re.search(r"\[(\d+)\]", line) if m: idx = int(m.group(1)) break @@ -355,18 +372,24 @@ def test_02_type_text_chars(self) -> None: print(f" text field element_index: {idx}") # Focus the text field via AX click first - c.call_tool("click", { - "pid": self._safari_pid, - "window_id": window_id, - "element_index": idx, - }) + c.call_tool( + "click", + { + "pid": self._safari_pid, + "window_id": window_id, + "element_index": idx, + }, + ) time.sleep(0.3) # Type via keystroke synthesis - result = c.call_tool("type_text_chars", { - "pid": self._safari_pid, - "text": "hello bg", - }) + result = c.call_tool( + "type_text_chars", + { + "pid": self._safari_pid, + "text": "hello bg", + }, + ) print(f" type_text_chars result: {result}") # Check if the text appears in the AX tree @@ -375,7 +398,7 @@ def test_02_type_text_chars(self) -> None: time.sleep(0.5) with DriverClient(self.binary) as c: tree = _get_page_text(c, self._safari_pid) - has_text = ("hello bg" in tree) + has_text = "hello bg" in tree if has_text: break print(f" attempt {attempt+1}: text not yet visible in tree") @@ -396,11 +419,14 @@ def test_03_pixel_click_no_focus_steal(self) -> None: """ with DriverClient(self.binary) as c: window_id = resolve_window_id(c, self._safari_pid) - snap = c.call_tool("get_window_state", { - "pid": self._safari_pid, - "window_id": window_id, - "query": "Click Me", - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._safari_pid, + "window_id": window_id, + "query": "Click Me", + }, + ) sc = snap.get("structuredContent", snap) width = sc.get("screenshot_width", 0) height = sc.get("screenshot_height", 0) @@ -410,12 +436,15 @@ def test_03_pixel_click_no_focus_steal(self) -> None: y = int(height * 0.35) print(f" pixel click at ({x}, {y})") - result = c.call_tool("click", { - "pid": self._safari_pid, - "window_id": window_id, - "x": x, - "y": y, - }) + result = c.call_tool( + "click", + { + "pid": self._safari_pid, + "window_id": window_id, + "x": x, + "y": y, + }, + ) print(f" click result: {result}") time.sleep(0.5) @@ -426,9 +455,13 @@ def test_03_pixel_click_no_focus_steal(self) -> None: def test_04_press_key_tab(self) -> None: """Send Tab key to backgrounded Safari without stealing focus.""" with DriverClient(self.binary) as c: - result = c.call_tool("press_key", { - "pid": self._safari_pid, "key": "tab", - }) + result = c.call_tool( + "press_key", + { + "pid": self._safari_pid, + "key": "tab", + }, + ) print(f"\n press_key result: {result}") time.sleep(0.3) @@ -476,9 +509,9 @@ def setUpClass(cls) -> None: with DriverClient(cls.binary) as c: active = frontmost_bundle_id(c) - assert active == FOCUS_MONITOR_BUNDLE, ( - f"Expected FocusMonitorApp frontmost, got {active}" - ) + assert ( + active == FOCUS_MONITOR_BUNDLE + ), f"Expected FocusMonitorApp frontmost, got {active}" @classmethod def tearDownClass(cls) -> None: @@ -506,12 +539,14 @@ def _assert_no_focus_loss(self, label: str) -> None: with DriverClient(self.binary) as c: active = frontmost_bundle_id(c) loss_delta = losses - self._losses_before - print(f" [{label}] losses: {self._losses_before}->{losses} " - f"(delta={loss_delta}), frontmost: {active}") + print( + f" [{label}] losses: {self._losses_before}->{losses} " + f"(delta={loss_delta}), frontmost: {active}" + ) self.assertEqual( - active, FOCUS_MONITOR_BUNDLE, - f"[{label}] Focus not restored — " - f"frontmost is {active}, not FocusMonitorApp", + active, + FOCUS_MONITOR_BUNDLE, + f"[{label}] Focus not restored — " f"frontmost is {active}, not FocusMonitorApp", ) def test_01_ax_click_2_plus_2(self) -> None: @@ -539,19 +574,25 @@ def test_01_ax_click_2_plus_2(self) -> None: # Press 2 + 2 = for idx in [btn_2, btn_add, btn_2, btn_eq]: - c.call_tool("click", { - "pid": self._calc_pid, - "window_id": window_id, - "element_index": idx, - }) + c.call_tool( + "click", + { + "pid": self._calc_pid, + "window_id": window_id, + "element_index": idx, + }, + ) time.sleep(0.3) # Read result - snap = c.call_tool("get_window_state", { - "pid": self._calc_pid, - "window_id": window_id, - "query": "AXStaticText", - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._calc_pid, + "window_id": window_id, + "query": "AXStaticText", + }, + ) tree = snap.get("structuredContent", snap).get("tree_markdown", "") print(f" result tree:\n{tree}") @@ -563,19 +604,26 @@ def test_02_pixel_click_buttons(self) -> None: with DriverClient(self.binary) as c: window_id = resolve_window_id(c, self._calc_pid) # Clear calculator first via AX (C button) - snap = c.call_tool("get_window_state", { - "pid": self._calc_pid, "window_id": window_id, - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._calc_pid, + "window_id": window_id, + }, + ) tree = snap.get("structuredContent", snap).get("tree_markdown", "") btn_c = _find_calc_button(tree, "Clear") if btn_c is None: btn_c = _find_calc_button(tree, "All clear") if btn_c is not None: - c.call_tool("click", { - "pid": self._calc_pid, - "window_id": window_id, - "element_index": btn_c, - }) + c.call_tool( + "click", + { + "pid": self._calc_pid, + "window_id": window_id, + "element_index": btn_c, + }, + ) time.sleep(0.3) # Re-activate focus monitor after the AX clear (may have stolen focus) @@ -583,9 +631,13 @@ def test_02_pixel_click_buttons(self) -> None: self._losses_before = _read_focus_losses() # Get fresh snapshot - snap = c.call_tool("get_window_state", { - "pid": self._calc_pid, "window_id": window_id, - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._calc_pid, + "window_id": window_id, + }, + ) tree = snap.get("structuredContent", snap).get("tree_markdown", "") sc = snap.get("structuredContent", snap) width = sc.get("screenshot_width", 0) @@ -596,18 +648,24 @@ def test_02_pixel_click_buttons(self) -> None: self.assertIsNotNone(btn_5, "'5' button not found") # Use AX to click 5, verify it works - c.call_tool("click", { - "pid": self._calc_pid, - "window_id": window_id, - "element_index": btn_5, - }) + c.call_tool( + "click", + { + "pid": self._calc_pid, + "window_id": window_id, + "element_index": btn_5, + }, + ) time.sleep(0.3) - snap = c.call_tool("get_window_state", { - "pid": self._calc_pid, - "window_id": window_id, - "query": "AXStaticText", - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._calc_pid, + "window_id": window_id, + "query": "AXStaticText", + }, + ) tree = snap.get("structuredContent", snap).get("tree_markdown", "") print(f" after AX click 5:\n{tree}") self.assertIn("5", tree, "AX click on '5' didn't register") @@ -619,25 +677,36 @@ def test_03_ax_click_3_plus_4(self) -> None: with DriverClient(self.binary) as c: window_id = resolve_window_id(c, self._calc_pid) # Clear first - snap = c.call_tool("get_window_state", { - "pid": self._calc_pid, "window_id": window_id, - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._calc_pid, + "window_id": window_id, + }, + ) tree = snap.get("structuredContent", snap).get("tree_markdown", "") btn_c = _find_calc_button(tree, "All Clear") if btn_c is None: btn_c = _find_calc_button(tree, "Clear") if btn_c is not None: - c.call_tool("click", { - "pid": self._calc_pid, - "window_id": window_id, - "element_index": btn_c, - }) + c.call_tool( + "click", + { + "pid": self._calc_pid, + "window_id": window_id, + "element_index": btn_c, + }, + ) time.sleep(0.3) # Find buttons for 3+4= - snap = c.call_tool("get_window_state", { - "pid": self._calc_pid, "window_id": window_id, - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._calc_pid, + "window_id": window_id, + }, + ) tree = snap.get("structuredContent", snap).get("tree_markdown", "") btn_3 = _find_calc_button(tree, "3") btn_add = _find_calc_button(tree, "Add") @@ -651,18 +720,24 @@ def test_03_ax_click_3_plus_4(self) -> None: self.assertIsNotNone(btn_eq, "'Equals' button not found") for idx in [btn_3, btn_add, btn_4, btn_eq]: - c.call_tool("click", { - "pid": self._calc_pid, - "window_id": window_id, - "element_index": idx, - }) + c.call_tool( + "click", + { + "pid": self._calc_pid, + "window_id": window_id, + "element_index": idx, + }, + ) time.sleep(0.3) - snap = c.call_tool("get_window_state", { - "pid": self._calc_pid, - "window_id": window_id, - "query": "AXStaticText", - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._calc_pid, + "window_id": window_id, + "query": "AXStaticText", + }, + ) tree = snap.get("structuredContent", snap).get("tree_markdown", "") print(f" result tree:\n{tree}") diff --git a/libs/cua-driver/Tests/integration/test_background_menu_shortcut.py b/libs/cua-driver/Tests/integration/test_background_menu_shortcut.py index 6be6f49d60..79430fa746 100644 --- a/libs/cua-driver/Tests/integration/test_background_menu_shortcut.py +++ b/libs/cua-driver/Tests/integration/test_background_menu_shortcut.py @@ -39,7 +39,8 @@ def _on_screen_doc_windows(driver, pid: int) -> list[dict]: result = driver.call_tool("list_windows", {"pid": pid}) windows = result.get("structuredContent", {}).get("windows", []) return [ - w for w in windows + w + for w in windows if w.get("is_on_screen") and (w.get("bounds", {}).get("width", 0) or 0) > 100 and (w.get("bounds", {}).get("height", 0) or 0) > 100 @@ -48,6 +49,7 @@ def _on_screen_doc_windows(driver, pid: int) -> list[dict]: # ── module-scoped driver (avoids scope-mismatch with module fixtures) ───────── + @pytest.fixture(scope="module") def module_driver(binary): """Module-scoped driver used by setup fixtures (textedit_pid, textedit_wid). @@ -56,22 +58,25 @@ def module_driver(binary): module-scoped fixtures must use this dedicated module_driver instead. """ from harness.driver import Driver + with Driver(binary) as d: yield d # ── module fixtures ─────────────────────────────────────────────────────────── + def _pid_from_result(result: dict) -> int: """Extract pid from a launch_app result (structuredContent or text fallback).""" import re + pid = result.get("structuredContent", {}).get("pid", 0) if pid: return pid # Fall back: parse "pid N" from the text content for item in result.get("content", []): text = item.get("text", "") - m = re.search(r'\bpid\s+(\d+)', text) + m = re.search(r"\bpid\s+(\d+)", text) if m: return int(m.group(1)) return 0 @@ -98,14 +103,16 @@ def textedit_pid(module_driver): def textedit_wid(module_driver, textedit_pid): """Return the window_id of the largest TextEdit on-screen window.""" wins = _on_screen_doc_windows(module_driver, textedit_pid) - best = max(wins, key=lambda w: ( - w.get("bounds", {}).get("width", 0) * w.get("bounds", {}).get("height", 0) - )) + best = max( + wins, + key=lambda w: (w.get("bounds", {}).get("width", 0) * w.get("bounds", {}).get("height", 0)), + ) return best["window_id"] # ── tests ───────────────────────────────────────────────────────────────────── + def test_hotkey_without_window_id_does_not_activate( driver, focus_monitor, activate_focus_monitor, ux_guard, textedit_pid ): @@ -114,17 +121,19 @@ def test_hotkey_without_window_id_does_not_activate( Uses the auth-envelope path which delivers directly to the target PID without calling activateForMenuShortcut — FocusMonitorApp keeps focus. """ - driver.call_tool("hotkey", { - "pid": textedit_pid, - "keys": ["cmd", "z"], - }) + driver.call_tool( + "hotkey", + { + "pid": textedit_pid, + "keys": ["cmd", "z"], + }, + ) time.sleep(0.5) # ux_guard.assert_clean() fires in teardown def test_hotkey_with_window_id_fires_nsmenu( - driver, focus_monitor, activate_focus_monitor, ux_guard, - textedit_pid, textedit_wid + driver, focus_monitor, activate_focus_monitor, ux_guard, textedit_pid, textedit_wid ): """hotkey WITH window_id fires NSMenu key equivalents with zero UX violations. @@ -136,11 +145,14 @@ def test_hotkey_with_window_id_fires_nsmenu( """ count_before = len(_on_screen_doc_windows(driver, textedit_pid)) - driver.call_tool("hotkey", { - "pid": textedit_pid, - "keys": ["cmd", "n"], - "window_id": textedit_wid, - }) + driver.call_tool( + "hotkey", + { + "pid": textedit_pid, + "keys": ["cmd", "n"], + "window_id": textedit_wid, + }, + ) time.sleep(1.5) count_after = len(_on_screen_doc_windows(driver, textedit_pid)) @@ -150,11 +162,11 @@ def test_hotkey_with_window_id_fires_nsmenu( wins = _on_screen_doc_windows(driver, textedit_pid) for w in wins[count_before:]: wid = w["window_id"] - driver.call_tool("hotkey", { - "pid": textedit_pid, "keys": ["cmd", "w"], "window_id": wid}) + driver.call_tool( + "hotkey", {"pid": textedit_pid, "keys": ["cmd", "w"], "window_id": wid} + ) time.sleep(0.4) - driver.call_tool("press_key", { - "pid": textedit_pid, "key": "delete", "window_id": wid}) + driver.call_tool("press_key", {"pid": textedit_pid, "key": "delete", "window_id": wid}) time.sleep(0.3) assert count_after > count_before, ( diff --git a/libs/cua-driver/Tests/integration/test_blender.py b/libs/cua-driver/Tests/integration/test_blender.py index b0455e6055..64bf9aa72b 100644 --- a/libs/cua-driver/Tests/integration/test_blender.py +++ b/libs/cua-driver/Tests/integration/test_blender.py @@ -19,19 +19,19 @@ from __future__ import annotations +import json +import os import subprocess import sys import time -import os -import json import pytest _HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, _HERE) -from harness.driver import Driver from harness import tree as Tree -from harness.cv import decode, crop, diff_ratio, save_reference, load_reference +from harness.cv import crop, decode, diff_ratio, load_reference, save_reference +from harness.driver import Driver BLENDER_BUNDLE = "org.blenderfoundation.blender" _ASSETS_BLENDER = os.path.join(_HERE, "assets", "blender") @@ -53,6 +53,7 @@ def _save_bboxes(bboxes: dict) -> None: # ── module-level Blender setup ──────────────────────────────────────────────── + @pytest.fixture(scope="module") def blender_pid(binary, focus_monitor): """Launch Blender in background; return its pid.""" @@ -84,8 +85,11 @@ def blender_pid(binary, focus_monitor): def _reactivate_focus(focus_monitor): _, pid = focus_monitor subprocess.run( - ["osascript", "-e", - f'tell application "System Events" to set frontmost of (first process whose unix id is {pid}) to true'], + [ + "osascript", + "-e", + f'tell application "System Events" to set frontmost of (first process whose unix id is {pid}) to true', + ], check=False, ) time.sleep(0.4) @@ -93,6 +97,7 @@ def _reactivate_focus(focus_monitor): # ── tests ───────────────────────────────────────────────────────────────────── + class TestBlenderWindow: """Basic window state and AX tree tests.""" diff --git a/libs/cua-driver/Tests/integration/test_browser_js.py b/libs/cua-driver/Tests/integration/test_browser_js.py index 528469894e..57e1b86ef7 100644 --- a/libs/cua-driver/Tests/integration/test_browser_js.py +++ b/libs/cua-driver/Tests/integration/test_browser_js.py @@ -42,6 +42,7 @@ # Helpers # --------------------------------------------------------------------------- + def _tool_text(result: dict) -> str: """Extract the text string from a tool call result.""" for item in result.get("content", []): @@ -54,14 +55,13 @@ def _enable_chrome_apple_events() -> None: """Quit Chrome, write allow_javascript_apple_events to all profiles, relaunch.""" subprocess.run( ["osascript", "-e", 'quit app "Google Chrome"'], - check=False, timeout=10, + check=False, + timeout=10, ) time.sleep(1.5) for prefs_path in glob.glob( - os.path.expanduser( - "~/Library/Application Support/Google/Chrome/*/Preferences" - ) + os.path.expanduser("~/Library/Application Support/Google/Chrome/*/Preferences") ): profile = prefs_path.split("/")[-2] if "System" in profile or "Guest" in profile: @@ -99,14 +99,14 @@ def _main_window(client: DriverClient, pid: int) -> int: # Prefer a window whose title contains "Example Domain". for line in text.splitlines(): if "Example Domain" in line or "example.com" in line.lower(): - m = re.search(r'\[window_id:\s*(\d+)\]', line) + m = re.search(r"\[window_id:\s*(\d+)\]", line) if m: return int(m.group(1)) # Fall back to first titled window. titled = re.findall(r'"[^"]+"\s+\[window_id:\s*(\d+)\]', text) if titled: return int(titled[0]) - m = re.search(r'\[window_id:\s*(\d+)\]', text) + m = re.search(r"\[window_id:\s*(\d+)\]", text) if m: return int(m.group(1)) raise RuntimeError(f"No window found for Chrome pid {pid}") @@ -116,6 +116,7 @@ def _main_window(client: DriverClient, pid: int) -> int: # Test class # --------------------------------------------------------------------------- + class BrowserJSTests(unittest.TestCase): """Browser JS primitives work end-to-end against a live Chrome window.""" @@ -131,13 +132,16 @@ def setUpClass(cls) -> None: # launch_app opens the URL in a new window WITHOUT stealing focus — # the driver's FocusRestoreGuard catches Chrome's activate call. with DriverClient(cls.binary) as c: - result = c.call_tool("launch_app", { - "bundle_id": CHROME_BUNDLE, - "urls": [TEST_URL], - }) + result = c.call_tool( + "launch_app", + { + "bundle_id": CHROME_BUNDLE, + "urls": [TEST_URL], + }, + ) text = _tool_text(result) # Extract pid from launch_app response. - m = re.search(r'pid[=:\s]+(\d+)', text, re.IGNORECASE) + m = re.search(r"pid[=:\s]+(\d+)", text, re.IGNORECASE) cls._chrome_pid = int(m.group(1)) if m else _find_chrome(c) # Wait for Chrome to load the page and update the window title. @@ -167,39 +171,48 @@ def setUpClass(cls) -> None: def test_01_execute_javascript_arithmetic(self) -> None: """execute_javascript returns the JS evaluation result.""" with DriverClient(self.binary) as c: - result = c.call_tool("page", { - "pid": self._chrome_pid, - "window_id": self._window_id, - "action": "execute_javascript", - "javascript": "1 + 1", - }) + result = c.call_tool( + "page", + { + "pid": self._chrome_pid, + "window_id": self._window_id, + "action": "execute_javascript", + "javascript": "1 + 1", + }, + ) self.assertFalse(result.get("isError")) self.assertIn("2", _tool_text(result)) def test_02_execute_javascript_dom_read(self) -> None: """execute_javascript can read DOM content.""" with DriverClient(self.binary) as c: - result = c.call_tool("page", { - "pid": self._chrome_pid, - "window_id": self._window_id, - "action": "execute_javascript", - "javascript": "document.querySelector('h1').innerText", - }) + result = c.call_tool( + "page", + { + "pid": self._chrome_pid, + "window_id": self._window_id, + "action": "execute_javascript", + "javascript": "document.querySelector('h1').innerText", + }, + ) self.assertFalse(result.get("isError")) self.assertIn(EXPECTED_H1, _tool_text(result)) def test_03_execute_javascript_iife(self) -> None: """execute_javascript handles IIFE with try-catch.""" with DriverClient(self.binary) as c: - result = c.call_tool("page", { - "pid": self._chrome_pid, - "window_id": self._window_id, - "action": "execute_javascript", - "javascript": ( - "(() => { try { return document.title; } " - "catch(e) { return 'error: ' + e; } })()" - ), - }) + result = c.call_tool( + "page", + { + "pid": self._chrome_pid, + "window_id": self._window_id, + "action": "execute_javascript", + "javascript": ( + "(() => { try { return document.title; } " + "catch(e) { return 'error: ' + e; } })()" + ), + }, + ) self.assertFalse(result.get("isError")) # ----------------------------------------------------------------------- @@ -209,25 +222,32 @@ def test_03_execute_javascript_iife(self) -> None: def test_04_get_text_returns_body_text(self) -> None: """get_text returns document.body.innerText containing the H1.""" with DriverClient(self.binary) as c: - result = c.call_tool("page", { - "pid": self._chrome_pid, - "window_id": self._window_id, - "action": "get_text", - }) + result = c.call_tool( + "page", + { + "pid": self._chrome_pid, + "window_id": self._window_id, + "action": "get_text", + }, + ) self.assertFalse(result.get("isError")) self.assertIn(EXPECTED_H1, _tool_text(result)) def test_05_get_text_includes_link_text(self) -> None: """get_text includes anchor text from the page.""" with DriverClient(self.binary) as c: - result = c.call_tool("page", { - "pid": self._chrome_pid, - "window_id": self._window_id, - "action": "get_text", - }) + result = c.call_tool( + "page", + { + "pid": self._chrome_pid, + "window_id": self._window_id, + "action": "get_text", + }, + ) text = _tool_text(result) - self.assertIn(EXPECTED_LINK_TEXT, text, - f"Expected link text in body text, got: {text[:200]!r}") + self.assertIn( + EXPECTED_LINK_TEXT, text, f"Expected link text in body text, got: {text[:200]!r}" + ) # ----------------------------------------------------------------------- # page action=query_dom @@ -236,13 +256,16 @@ def test_05_get_text_includes_link_text(self) -> None: def test_06_query_dom_returns_json_array(self) -> None: """query_dom returns a JSON array of matching elements with hrefs.""" with DriverClient(self.binary) as c: - result = c.call_tool("page", { - "pid": self._chrome_pid, - "window_id": self._window_id, - "action": "query_dom", - "css_selector": "a[href]", - "attributes": ["href"], - }) + result = c.call_tool( + "page", + { + "pid": self._chrome_pid, + "window_id": self._window_id, + "action": "query_dom", + "css_selector": "a[href]", + "attributes": ["href"], + }, + ) self.assertFalse(result.get("isError")) text = _tool_text(result) self.assertIn("```json", text) @@ -259,12 +282,15 @@ def test_06_query_dom_returns_json_array(self) -> None: def test_07_query_dom_h1_no_attributes(self) -> None: """query_dom without attributes returns tag and text.""" with DriverClient(self.binary) as c: - result = c.call_tool("page", { - "pid": self._chrome_pid, - "window_id": self._window_id, - "action": "query_dom", - "css_selector": "h1", - }) + result = c.call_tool( + "page", + { + "pid": self._chrome_pid, + "window_id": self._window_id, + "action": "query_dom", + "css_selector": "h1", + }, + ) self.assertFalse(result.get("isError")) text = _tool_text(result) json_block = text.split("```json")[-1].split("```")[0].strip() @@ -280,11 +306,14 @@ def test_07_query_dom_h1_no_attributes(self) -> None: def test_08_get_window_state_javascript_co_located(self) -> None: """get_window_state with javascript= returns JS result alongside AX tree.""" with DriverClient(self.binary) as c: - result = c.call_tool("get_window_state", { - "pid": self._chrome_pid, - "window_id": self._window_id, - "javascript": "document.title", - }) + result = c.call_tool( + "get_window_state", + { + "pid": self._chrome_pid, + "window_id": self._window_id, + "javascript": "document.title", + }, + ) self.assertFalse(result.get("isError")) text = _tool_text(result) self.assertIn("## JavaScript result", text) @@ -296,11 +325,14 @@ def test_09_get_window_state_javascript_error_is_inline(self) -> None: # Chrome returns "missing value" for JS that throws — osascript exits 0. # The result section appears but may contain "missing value" or an error msg. with DriverClient(self.binary) as c: - result = c.call_tool("get_window_state", { - "pid": self._chrome_pid, - "window_id": self._window_id, - "javascript": "undefined_var_that_does_not_exist", - }) + result = c.call_tool( + "get_window_state", + { + "pid": self._chrome_pid, + "window_id": self._window_id, + "javascript": "undefined_var_that_does_not_exist", + }, + ) # isError must be False — JS failures must not kill the whole tool call. self.assertFalse(result.get("isError")) text = _tool_text(result) @@ -314,21 +346,27 @@ def test_09_get_window_state_javascript_error_is_inline(self) -> None: def test_10_page_execute_missing_javascript_field(self) -> None: """page execute_javascript returns isError when javascript field is absent.""" with DriverClient(self.binary) as c: - result = c.call_tool("page", { - "pid": self._chrome_pid, - "window_id": self._window_id, - "action": "execute_javascript", - }) + result = c.call_tool( + "page", + { + "pid": self._chrome_pid, + "window_id": self._window_id, + "action": "execute_javascript", + }, + ) self.assertTrue(result.get("isError")) def test_11_page_query_dom_missing_selector(self) -> None: """page query_dom returns isError when css_selector is absent.""" with DriverClient(self.binary) as c: - result = c.call_tool("page", { - "pid": self._chrome_pid, - "window_id": self._window_id, - "action": "query_dom", - }) + result = c.call_tool( + "page", + { + "pid": self._chrome_pid, + "window_id": self._window_id, + "action": "query_dom", + }, + ) self.assertTrue(result.get("isError")) diff --git a/libs/cua-driver/Tests/integration/test_check_permissions_cli.py b/libs/cua-driver/Tests/integration/test_check_permissions_cli.py index 6576670c2f..8dc16b290b 100644 --- a/libs/cua-driver/Tests/integration/test_check_permissions_cli.py +++ b/libs/cua-driver/Tests/integration/test_check_permissions_cli.py @@ -35,7 +35,6 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from driver_client import default_binary_path - WARNING_MARKER = "Not running inside the cua-driver daemon process" @@ -51,9 +50,7 @@ def _run_cli(binary: str, *args: str) -> subprocess.CompletedProcess: def _stop_daemon(binary: str) -> None: """Best-effort stop; the 'not running' case is not an error here.""" - subprocess.run( - [binary, "stop"], capture_output=True, text=True, timeout=5 - ) + subprocess.run([binary, "stop"], capture_output=True, text=True, timeout=5) # Let the socket file vanish before the next call. time.sleep(0.3) @@ -64,12 +61,8 @@ def _start_daemon(binary: str) -> None: inherits the shell's TCC context, defeating the point of the test.""" # Resolve the .app path from the binary path. The build script puts # the binary at `.build/CuaDriver.app/Contents/MacOS/cua-driver`. - app_path = os.path.abspath( - os.path.join(os.path.dirname(binary), "..", "..") - ) - assert app_path.endswith(".app"), ( - f"expected binary inside CuaDriver.app, got {app_path!r}" - ) + app_path = os.path.abspath(os.path.join(os.path.dirname(binary), "..", "..")) + assert app_path.endswith(".app"), f"expected binary inside CuaDriver.app, got {app_path!r}" subprocess.run( ["open", "-a", app_path, "--args", "serve"], check=True, @@ -79,9 +72,7 @@ def _start_daemon(binary: str) -> None: # PermissionsGate before binding the socket, so this can take ~1s. deadline = time.monotonic() + 5.0 while time.monotonic() < deadline: - result = subprocess.run( - [binary, "status"], capture_output=True, text=True, timeout=2 - ) + result = subprocess.run([binary, "status"], capture_output=True, text=True, timeout=2) if result.returncode == 0: return time.sleep(0.1) diff --git a/libs/cua-driver/Tests/integration/test_chrome.py b/libs/cua-driver/Tests/integration/test_chrome.py index 26dcf066db..c5f235bbfc 100644 --- a/libs/cua-driver/Tests/integration/test_chrome.py +++ b/libs/cua-driver/Tests/integration/test_chrome.py @@ -9,24 +9,25 @@ from __future__ import annotations +import os import re import subprocess import sys import time -import os import pytest _HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, _HERE) -from harness.driver import Driver from harness import tree as Tree +from harness.driver import Driver def _parse_clicks(tree_text: str) -> int: - m = re.search(r'clicks:\s*(\d+)', tree_text) + m = re.search(r"clicks:\s*(\d+)", tree_text) return int(m.group(1)) if m else 0 + CHROME_BUNDLE = "com.google.Chrome" FOCUS_MONITOR_BUNDLE = "com.trycua.FocusMonitorApp" @@ -52,6 +53,7 @@ def chrome_pid(binary, html_server, focus_monitor): # Use a throwaway profile dir so Chrome never shows restore/first-run dialogs import tempfile + _tmp_profile = tempfile.mkdtemp(prefix="cua_chrome_test_") page_url = f"{html_server}/test_page.html" @@ -82,8 +84,11 @@ def chrome_pid(binary, html_server, focus_monitor): # Hand focus to FocusMonitorApp by pid to avoid activating stale instances _, fm_pid = focus_monitor subprocess.run( - ["osascript", "-e", - f'tell application "System Events" to set frontmost of (first process whose unix id is {fm_pid}) to true'], + [ + "osascript", + "-e", + f'tell application "System Events" to set frontmost of (first process whose unix id is {fm_pid}) to true', + ], check=False, ) time.sleep(0.5) @@ -98,8 +103,11 @@ def chrome_pid(binary, html_server, focus_monitor): def _reactivate_focus(focus_monitor): _, pid = focus_monitor subprocess.run( - ["osascript", "-e", - f'tell application "System Events" to set frontmost of (first process whose unix id is {pid}) to true'], + [ + "osascript", + "-e", + f'tell application "System Events" to set frontmost of (first process whose unix id is {pid}) to true', + ], check=False, ) time.sleep(0.4) @@ -107,13 +115,16 @@ def _reactivate_focus(focus_monitor): # ── tests ───────────────────────────────────────────────────────────────────── + class TestChromeButton: def test_click_increments_counter(self, driver, chrome_pid, ux_guard): wid = driver.find_window(chrome_pid, probe_ax=True) full = driver.get_window_state(chrome_pid, wid) idx = full.find_element("Click Me") - assert idx is not None, f"'Click Me' not found in tree (first 800 chars):\n{full.tree[:800]}" + assert ( + idx is not None + ), f"'Click Me' not found in tree (first 800 chars):\n{full.tree[:800]}" count_before = _parse_clicks(full.tree) driver.click(chrome_pid, wid, element_index=idx) @@ -122,7 +133,9 @@ def test_click_increments_counter(self, driver, chrome_pid, ux_guard): after = driver.get_window_state(chrome_pid, wid) count_after = _parse_clicks(after.tree) print(f"\n click counter: {count_before} → {count_after}") - assert count_after == count_before + 1, f"Counter did not increment: {count_before} → {count_after}" + assert ( + count_after == count_before + 1 + ), f"Counter did not increment: {count_before} → {count_after}" def test_click_increments_twice(self, driver, chrome_pid, ux_guard): wid = driver.find_window(chrome_pid, probe_ax=True) @@ -139,7 +152,9 @@ def test_click_increments_twice(self, driver, chrome_pid, ux_guard): after = driver.get_window_state(chrome_pid, wid) count_after = _parse_clicks(after.tree) print(f"\n click counter: {count_before} → {count_after}") - assert count_after == count_before + 2, f"Counter should have incremented by 2: {count_before} → {count_after}" + assert ( + count_after == count_before + 2 + ), f"Counter should have incremented by 2: {count_before} → {count_after}" class TestChromeTextInput: @@ -172,6 +187,7 @@ def test_canvas_pixel_click_no_focus_steal(self, driver, chrome_pid, ux_guard): assert mid.screenshot_b64, "No screenshot" from harness.cv import decode, diff_ratio + before_img = decode(mid.screenshot_b64) x = mid.screenshot_width // 2 diff --git a/libs/cua-driver/Tests/integration/test_chrome_minimized_nav.py b/libs/cua-driver/Tests/integration/test_chrome_minimized_nav.py index 0a5816ea2b..d06dc15e56 100644 --- a/libs/cua-driver/Tests/integration/test_chrome_minimized_nav.py +++ b/libs/cua-driver/Tests/integration/test_chrome_minimized_nav.py @@ -38,9 +38,7 @@ _REPO_ROOT = os.path.dirname(os.path.dirname(_THIS_DIR)) _FOCUS_APP_DIR = os.path.join(_REPO_ROOT, "Tests", "FocusMonitorApp") _FOCUS_APP_BUNDLE = os.path.join(_FOCUS_APP_DIR, "FocusMonitorApp.app") -_FOCUS_APP_EXE = os.path.join( - _FOCUS_APP_BUNDLE, "Contents", "MacOS", "FocusMonitorApp" -) +_FOCUS_APP_EXE = os.path.join(_FOCUS_APP_BUNDLE, "Contents", "MacOS", "FocusMonitorApp") _LOSS_FILE = "/tmp/focus_monitor_losses.txt" CHROME_BUNDLE = "com.google.Chrome" @@ -51,11 +49,10 @@ # Helpers # --------------------------------------------------------------------------- + def _build_focus_app() -> None: if not os.path.exists(_FOCUS_APP_EXE): - subprocess.run( - [os.path.join(_FOCUS_APP_DIR, "build.sh")], check=True - ) + subprocess.run([os.path.join(_FOCUS_APP_DIR, "build.sh")], check=True) def _launch_focus_app() -> tuple[subprocess.Popen, int]: @@ -85,7 +82,7 @@ def _read_focus_losses() -> int: def _find_element_index(tree: str, label: str) -> int | None: for line in tree.split("\n"): if label in line: - m = re.search(r'\[(\d+)\]', line) + m = re.search(r"\[(\d+)\]", line) if m: return int(m.group(1)) return None @@ -94,9 +91,7 @@ def _find_element_index(tree: str, label: str) -> int | None: def _chrome_is_minimized(client: DriverClient, pid: int) -> bool: """Check if Chrome's main window is minimized by looking for AXMinimized.""" window_id = resolve_window_id(client, pid, require_on_current_space=False) - snap = client.call_tool( - "get_window_state", {"pid": pid, "window_id": window_id} - ) + snap = client.call_tool("get_window_state", {"pid": pid, "window_id": window_id}) tree = snap.get("structuredContent", snap).get("tree_markdown", "") # A minimized window typically won't have on-screen content, # but we can check the has_screenshot field — if no on-screen @@ -117,6 +112,7 @@ def _activate_focus_monitor() -> None: # Test class # --------------------------------------------------------------------------- + class ChromeMinimizedNavTests(unittest.TestCase): """Interact with a minimized Chrome without deminiaturizing or stealing focus.""" @@ -141,20 +137,26 @@ def setUpClass(cls) -> None: if chrome: cls._chrome_pid = chrome[0]["pid"] else: - result = c.call_tool("launch_app", { - "bundle_id": CHROME_BUNDLE, - "urls": ["about:blank"], - }) + result = c.call_tool( + "launch_app", + { + "bundle_id": CHROME_BUNDLE, + "urls": ["about:blank"], + }, + ) cls._chrome_pid = result["structuredContent"]["pid"] time.sleep(2.0) print(f"\n Chrome pid: {cls._chrome_pid}") # Ensure Chrome has an about:blank window via launch_app (no focus steal). with DriverClient(cls.binary) as c: - c.call_tool("launch_app", { - "bundle_id": CHROME_BUNDLE, - "urls": ["about:blank"], - }) + c.call_tool( + "launch_app", + { + "bundle_id": CHROME_BUNDLE, + "urls": ["about:blank"], + }, + ) time.sleep(2.0) # Snapshot AX tree BEFORE minimizing to cache the omnibox index. @@ -163,10 +165,13 @@ def setUpClass(cls) -> None: # resolve cached indices. with DriverClient(cls.binary) as c: cls._chrome_window_id = resolve_window_id(c, cls._chrome_pid) - snap = c.call_tool("get_window_state", { - "pid": cls._chrome_pid, - "window_id": cls._chrome_window_id, - }) + snap = c.call_tool( + "get_window_state", + { + "pid": cls._chrome_pid, + "window_id": cls._chrome_window_id, + }, + ) tree = snap.get("structuredContent", snap).get("tree_markdown", "") cls._omnibox_idx = _find_element_index(tree, "Address and search bar") if cls._omnibox_idx is None: @@ -176,10 +181,13 @@ def setUpClass(cls) -> None: # Minimize Chrome via Cmd+M. with DriverClient(cls.binary) as c: - c.call_tool("hotkey", { - "pid": cls._chrome_pid, - "keys": ["cmd", "m"], - }) + c.call_tool( + "hotkey", + { + "pid": cls._chrome_pid, + "keys": ["cmd", "m"], + }, + ) time.sleep(1.5) print(" Chrome minimized via Cmd+M") @@ -190,9 +198,9 @@ def setUpClass(cls) -> None: with DriverClient(cls.binary) as c: active = frontmost_bundle_id(c) - assert active == FOCUS_MONITOR_BUNDLE, ( - f"Expected FocusMonitorApp frontmost, got {active}" - ) + assert ( + active == FOCUS_MONITOR_BUNDLE + ), f"Expected FocusMonitorApp frontmost, got {active}" @classmethod def tearDownClass(cls) -> None: @@ -203,8 +211,7 @@ def tearDownClass(cls) -> None: cls._focus_proc.kill() # Close the Chrome window we opened. subprocess.run( - ["osascript", "-e", - 'tell application "Google Chrome" to close window 1'], + ["osascript", "-e", 'tell application "Google Chrome" to close window 1'], check=False, ) try: @@ -216,10 +223,13 @@ def setUp(self) -> None: # Minimize Chrome via hotkey — cua-driver delivers Cmd+M to the pid # via CGEvent.postToPid without stealing focus. with DriverClient(self.binary) as c: - c.call_tool("hotkey", { - "pid": self._chrome_pid, - "keys": ["cmd", "m"], - }) + c.call_tool( + "hotkey", + { + "pid": self._chrome_pid, + "keys": ["cmd", "m"], + }, + ) time.sleep(1.5) _activate_focus_monitor() self._losses_before = _read_focus_losses() @@ -231,10 +241,13 @@ def _assert_focus_preserved(self, label: str) -> None: with DriverClient(self.binary) as c: active = frontmost_bundle_id(c) loss_delta = losses - self._losses_before - print(f" [{label}] losses: {self._losses_before}->{losses} " - f"(delta={loss_delta}), frontmost: {active}") + print( + f" [{label}] losses: {self._losses_before}->{losses} " + f"(delta={loss_delta}), frontmost: {active}" + ) self.assertEqual( - active, FOCUS_MONITOR_BUNDLE, + active, + FOCUS_MONITOR_BUNDLE, f"[{label}] Focus stolen — frontmost is {active}", ) @@ -242,10 +255,13 @@ def _assert_chrome_still_minimized(self, label: str) -> None: """Chrome window is still minimized — verified by checking if get_window_state returns has_screenshot=false (no on-screen window).""" with DriverClient(self.binary) as c: - snap = c.call_tool("get_window_state", { - "pid": self._chrome_pid, - "window_id": self._chrome_window_id, - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._chrome_pid, + "window_id": self._chrome_window_id, + }, + ) sc = snap.get("structuredContent", snap) has_shot = sc.get("has_screenshot", False) is_minimized = not has_shot @@ -260,10 +276,13 @@ def _assert_chrome_still_minimized(self, label: str) -> None: def test_01_get_window_state_safe(self) -> None: """AX snapshot of minimized Chrome should not deminiaturize.""" with DriverClient(self.binary) as c: - snap = c.call_tool("get_window_state", { - "pid": self._chrome_pid, - "window_id": self._chrome_window_id, - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._chrome_pid, + "window_id": self._chrome_window_id, + }, + ) tree = snap.get("structuredContent", snap).get("tree_markdown", "") print(f"\n tree length: {len(tree)} chars") @@ -281,15 +300,21 @@ def test_02_ax_click_omnibox(self) -> None: with DriverClient(self.binary) as c: # Refresh element cache. - c.call_tool("get_window_state", { - "pid": self._chrome_pid, - "window_id": self._chrome_window_id, - }) - result = c.call_tool("click", { - "pid": self._chrome_pid, - "window_id": self._chrome_window_id, - "element_index": idx, - }) + c.call_tool( + "get_window_state", + { + "pid": self._chrome_pid, + "window_id": self._chrome_window_id, + }, + ) + result = c.call_tool( + "click", + { + "pid": self._chrome_pid, + "window_id": self._chrome_window_id, + "element_index": idx, + }, + ) text = "" for item in result.get("content", []): if item.get("type") == "text": @@ -310,16 +335,22 @@ def test_03_set_value_omnibox(self) -> None: self.skipTest("Omnibox not found in pre-minimize AX tree") with DriverClient(self.binary) as c: - c.call_tool("get_window_state", { - "pid": self._chrome_pid, - "window_id": self._chrome_window_id, - }) - result = c.call_tool("set_value", { - "pid": self._chrome_pid, - "window_id": self._chrome_window_id, - "element_index": idx, - "value": "https://example.com", - }) + c.call_tool( + "get_window_state", + { + "pid": self._chrome_pid, + "window_id": self._chrome_window_id, + }, + ) + result = c.call_tool( + "set_value", + { + "pid": self._chrome_pid, + "window_id": self._chrome_window_id, + "element_index": idx, + "value": "https://example.com", + }, + ) text = "" for item in result.get("content", []): if item.get("type") == "text": @@ -335,10 +366,13 @@ def test_03_set_value_omnibox(self) -> None: def test_04_type_without_omnibox_focus(self) -> None: """Type keys to minimized Chrome without focusing omnibox — should stay minimized.""" with DriverClient(self.binary) as c: - c.call_tool("type_text_chars", { - "pid": self._chrome_pid, - "text": "hello", - }) + c.call_tool( + "type_text_chars", + { + "pid": self._chrome_pid, + "text": "hello", + }, + ) time.sleep(0.5) self._assert_chrome_still_minimized("04_type_no_omnibox") @@ -355,10 +389,13 @@ def test_05_focus_without_raise_safe(self) -> None: with DriverClient(self.binary) as c: # get_window_state uses FocusWithoutRaise internally for # AX enablement. Just verify the snapshot path is safe. - snap = c.call_tool("get_window_state", { - "pid": self._chrome_pid, - "window_id": self._chrome_window_id, - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._chrome_pid, + "window_id": self._chrome_window_id, + }, + ) count = snap.get("structuredContent", snap).get("element_count", 0) print(f"\n element_count: {count}") diff --git a/libs/cua-driver/Tests/integration/test_click_opens_new_window.py b/libs/cua-driver/Tests/integration/test_click_opens_new_window.py index c8c7df5222..5700eae98c 100644 --- a/libs/cua-driver/Tests/integration/test_click_opens_new_window.py +++ b/libs/cua-driver/Tests/integration/test_click_opens_new_window.py @@ -47,9 +47,7 @@ _REPO_ROOT = os.path.dirname(os.path.dirname(_THIS_DIR)) _FOCUS_APP_DIR = os.path.join(_REPO_ROOT, "Tests", "FocusMonitorApp") _FOCUS_APP_BUNDLE = os.path.join(_FOCUS_APP_DIR, "FocusMonitorApp.app") -_FOCUS_APP_EXE = os.path.join( - _FOCUS_APP_BUNDLE, "Contents", "MacOS", "FocusMonitorApp" -) +_FOCUS_APP_EXE = os.path.join(_FOCUS_APP_BUNDLE, "Contents", "MacOS", "FocusMonitorApp") _LOSS_FILE = "/tmp/focus_monitor_losses.txt" _UTM_BUNDLE = "com.utmapp.UTM" @@ -62,6 +60,7 @@ # Helpers # --------------------------------------------------------------------------- + def _tool_text(result: dict) -> str: for item in result.get("content", []): if item.get("type") == "text": @@ -122,6 +121,7 @@ def _kill(name: str) -> None: # Test # --------------------------------------------------------------------------- + class TestBrowseUTMGalleryUXGuard(unittest.TestCase): """Clicking 'Browse UTM Gallery' in a backgrounded UTM must not steal focus. @@ -181,9 +181,9 @@ def test_browse_gallery_click_with_ux_guard(self) -> None: # Verify FocusMonitorApp is frontmost before the click. front_before = frontmost_bundle_id(c) self.assertEqual( - front_before, _FOCUS_MONITOR_BUNDLE, - f"Expected FocusMonitorApp to be frontmost before the click, " - f"got {front_before!r}", + front_before, + _FOCUS_MONITOR_BUNDLE, + f"Expected FocusMonitorApp to be frontmost before the click, " f"got {front_before!r}", ) losses_before = _read_focus_losses() @@ -208,21 +208,27 @@ def test_browse_gallery_click_with_ux_guard(self) -> None: # Click the gallery button — by element index if found, pixel otherwise. if gallery_idx is not None: - result = c.call_tool("click", { - "pid": utm_pid, - "window_id": window_id, - "element_index": gallery_idx, - }) + result = c.call_tool( + "click", + { + "pid": utm_pid, + "window_id": window_id, + "element_index": gallery_idx, + }, + ) else: # Fallback: pixel click at the approximate "Browse UTM Gallery" # button location in UTM's welcome screen. b = utm_win["bounds"] - result = c.call_tool("click", { - "pid": utm_pid, - "window_id": window_id, - "x": b["width"] / 2.0, - "y": b["height"] * 0.35, - }) + result = c.call_tool( + "click", + { + "pid": utm_pid, + "window_id": window_id, + "x": b["width"] / 2.0, + "y": b["height"] * 0.35, + }, + ) result_text = _tool_text(result) @@ -236,11 +242,13 @@ def test_browse_gallery_click_with_ux_guard(self) -> None: # 2. Result must announce the new Safari window. self.assertIn( - "🪟", result_text, + "🪟", + result_text, f"Expected 🪟 new-window notice in click result.\nGot: {result_text}", ) self.assertIn( - "Safari", result_text, + "Safari", + result_text, f"Expected 'Safari' mentioned in new-window notice.\nGot: {result_text}", ) @@ -248,7 +256,8 @@ def test_browse_gallery_click_with_ux_guard(self) -> None: time.sleep(0.3) front_after = frontmost_bundle_id(c) self.assertEqual( - front_after, _FOCUS_MONITOR_BUNDLE, + front_after, + _FOCUS_MONITOR_BUNDLE, f"Expected FocusMonitorApp to remain frontmost after re-raise, " f"got {front_after!r}.\nClick result: {result_text}", ) @@ -264,7 +273,8 @@ def test_browse_gallery_click_with_ux_guard(self) -> None: losses_after = _read_focus_losses() delta = losses_after - losses_before self.assertLessEqual( - delta, 1, + delta, + 1, f"FocusMonitorApp lost focus {delta} time(s) (max 1 allowed) " f"during the click sequence — ux_guard violated.\n" f"Click result: {result_text}", diff --git a/libs/cua-driver/Tests/integration/test_click_pixel_ax.py b/libs/cua-driver/Tests/integration/test_click_pixel_ax.py index 68cafd0f99..645cdc3c8f 100644 --- a/libs/cua-driver/Tests/integration/test_click_pixel_ax.py +++ b/libs/cua-driver/Tests/integration/test_click_pixel_ax.py @@ -111,14 +111,10 @@ def test_pixel_click_dispatches_via_ax_hit_test(self) -> None: # hit-test cache. Force `som` (tree + screenshot, the shipped # default) so this assertion doesn't flap based on whatever # capture_mode the previous session left on disk. - set_mode = self.client.call_tool( - "set_config", {"key": "capture_mode", "value": "som"} - ) + set_mode = self.client.call_tool("set_config", {"key": "capture_mode", "value": "som"}) self.assertIsNone(set_mode.get("isError"), msg=set_mode) - launch = self.client.call_tool( - "launch_app", {"bundle_id": "com.apple.calculator"} - ) + launch = self.client.call_tool("launch_app", {"bundle_id": "com.apple.calculator"}) self.assertIsNone(launch.get("isError"), msg=launch) pid = launch["structuredContent"]["pid"] @@ -129,9 +125,7 @@ def test_pixel_click_dispatches_via_ax_hit_test(self) -> None: # Populate the snapshot cache so element_index paths work if # the test ever needs them; harmless for the pixel assertion. window_id = resolve_window_id(self.client, pid) - snap = self.client.call_tool( - "get_window_state", {"pid": pid, "window_id": window_id} - ) + snap = self.client.call_tool("get_window_state", {"pid": pid, "window_id": window_id}) self.assertIsNone(snap.get("isError"), msg=snap) scale = snap["structuredContent"].get("screenshot_scale_factor") or 1.0 diff --git a/libs/cua-driver/Tests/integration/test_drag_slider_delivery.py b/libs/cua-driver/Tests/integration/test_drag_slider_delivery.py index c58b152e11..d63eec3571 100644 --- a/libs/cua-driver/Tests/integration/test_drag_slider_delivery.py +++ b/libs/cua-driver/Tests/integration/test_drag_slider_delivery.py @@ -42,9 +42,7 @@ _FOCUS_APP_DIR = os.path.join(_REPO_ROOT, "Tests", "FocusMonitorApp") _FOCUS_APP_BUNDLE = os.path.join(_FOCUS_APP_DIR, "FocusMonitorApp.app") -_FOCUS_APP_EXE = os.path.join( - _FOCUS_APP_BUNDLE, "Contents", "MacOS", "FocusMonitorApp" -) +_FOCUS_APP_EXE = os.path.join(_FOCUS_APP_BUNDLE, "Contents", "MacOS", "FocusMonitorApp") _LOSS_FILE = "/tmp/focus_monitor_losses.txt" SAFARI_BUNDLE = "com.apple.Safari" @@ -53,9 +51,7 @@ def _build_focus_app() -> None: if not os.path.exists(_FOCUS_APP_EXE): - subprocess.run( - [os.path.join(_FOCUS_APP_DIR, "build.sh")], check=True - ) + subprocess.run([os.path.join(_FOCUS_APP_DIR, "build.sh")], check=True) def _launch_focus_app() -> tuple[subprocess.Popen, int]: @@ -77,7 +73,8 @@ def _launch_focus_app() -> tuple[subprocess.Popen, int]: raise RuntimeError("FocusMonitorApp did not print FOCUS_PID in time") subprocess.run( ["osascript", "-e", 'tell application "FocusMonitorApp" to activate'], - check=False, timeout=3, + check=False, + timeout=3, ) return proc, pid @@ -92,12 +89,12 @@ def _read_focus_losses() -> int: def _safari_js(expr: str, timeout: float = 5.0) -> str: """Evaluate a JS expression in Safari's front document via osascript.""" - script = ( - f'tell application "Safari" to do JavaScript "{expr}" in front document' - ) + script = f'tell application "Safari" to do JavaScript "{expr}" in front document' r = subprocess.run( ["osascript", "-e", script], - capture_output=True, text=True, timeout=timeout, + capture_output=True, + text=True, + timeout=timeout, ) return r.stdout.strip() @@ -186,9 +183,9 @@ def setUpClass(cls) -> None: with DriverClient(cls.binary) as c: active = frontmost_bundle_id(c) - assert active == FOCUS_MONITOR_BUNDLE, ( - f"Expected FocusMonitorApp frontmost at start, got {active}" - ) + assert ( + active == FOCUS_MONITOR_BUNDLE + ), f"Expected FocusMonitorApp frontmost at start, got {active}" losses = _read_focus_losses() assert losses == 0, f"Expected 0 focus losses at start, got {losses}" @@ -220,8 +217,7 @@ def test_slider_drag_to_right_edge(self) -> None: """Drag the thumb from mid-track to past the right edge — value goes to 100.""" before = _slider_value() self.assertEqual( - before, 50, - f"slider didn't reset to 50 (got {before}) — fixture or JS bridge off" + before, 50, f"slider didn't reset to 50 (got {before}) — fixture or JS bridge off" ) with DriverClient(self.binary) as c: @@ -234,28 +230,25 @@ def test_slider_drag_to_right_edge(self) -> None: scale = sc.get("screenshot_scale_factor", 2) # Window origin in screen points (top-left). - windows = c.call_tool( - "list_windows", {"pid": self._safari_pid} - )["structuredContent"]["windows"] - win = next( - w for w in windows - if w["window_id"] == window_id - ) + windows = c.call_tool("list_windows", {"pid": self._safari_pid})["structuredContent"][ + "windows" + ] + win = next(w for w in windows if w["window_id"] == window_id) win_x = win["bounds"]["x"] win_y = win["bounds"]["y"] rect = _slider_screen_rect() # Slider center in screen points. cx = rect["screenLeft"] + rect["left"] + rect["width"] / 2 - cy = rect["screenTop"] + rect["top"] + rect["height"] / 2 + cy = rect["screenTop"] + rect["top"] + rect["height"] / 2 # End point: 50 px past the right edge of the slider, same y. ex = rect["screenLeft"] + rect["left"] + rect["width"] + 50 ey = cy # Convert screen points → window-local image pixels. from_x = (cx - win_x) * scale from_y = (cy - win_y) * scale - to_x = (ex - win_x) * scale - to_y = (ey - win_y) * scale + to_x = (ex - win_x) * scale + to_y = (ey - win_y) * scale print( f"\n slider rect (screen pts): " @@ -276,9 +269,7 @@ def test_slider_drag_to_right_edge(self) -> None: "steps": 24, }, ) - print( - f" drag result: {result.get('content', [{}])[0].get('text', '')[:200]}" - ) + print(f" drag result: {result.get('content', [{}])[0].get('text', '')[:200]}") time.sleep(0.4) after = _slider_value() @@ -286,8 +277,7 @@ def test_slider_drag_to_right_edge(self) -> None: # Drag landed AND clamped at the slider's max (100). self.assertGreaterEqual( - after, 80, - f"Drag did not move the slider far enough — value is {after} (was {before})" + after, 80, f"Drag did not move the slider far enough — value is {after} (was {before})" ) # Focus invariant: FocusMonitorApp stayed frontmost throughout. @@ -296,12 +286,12 @@ def test_slider_drag_to_right_edge(self) -> None: losses = _read_focus_losses() print(f" losses: {self._losses_before}→{losses}, frontmost: {active}") self.assertEqual( - active, FOCUS_MONITOR_BUNDLE, - f"Focus stolen — frontmost is {active} (expected FocusMonitorApp)" + active, + FOCUS_MONITOR_BUNDLE, + f"Focus stolen — frontmost is {active} (expected FocusMonitorApp)", ) self.assertEqual( - losses, self._losses_before, - f"Focus losses increased: {self._losses_before} → {losses}" + losses, self._losses_before, f"Focus losses increased: {self._losses_before} → {losses}" ) diff --git a/libs/cua-driver/Tests/integration/test_electron.py b/libs/cua-driver/Tests/integration/test_electron.py index 5c9818ceca..777ad4eaa4 100644 --- a/libs/cua-driver/Tests/integration/test_electron.py +++ b/libs/cua-driver/Tests/integration/test_electron.py @@ -11,23 +11,24 @@ from __future__ import annotations +import os import subprocess import sys import time -import os import pytest try: import requests as _requests + _REQUESTS_AVAILABLE = True except ImportError: _REQUESTS_AVAILABLE = False _HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, _HERE) -from harness.driver import Driver from harness import tree as Tree +from harness.driver import Driver _ELECTRON_API = "http://localhost:6769" @@ -55,8 +56,11 @@ def _http_reset(base_url: str = _ELECTRON_API) -> None: def _reactivate_focus(focus_monitor): _, pid = focus_monitor subprocess.run( - ["osascript", "-e", - f'tell application "System Events" to set frontmost of (first process whose unix id is {pid}) to true'], + [ + "osascript", + "-e", + f'tell application "System Events" to set frontmost of (first process whose unix id is {pid}) to true', + ], check=False, ) time.sleep(0.4) @@ -64,6 +68,7 @@ def _reactivate_focus(focus_monitor): # ── tests ───────────────────────────────────────────────────────────────────── + class TestElectronButton: def test_click_increments_counter(self, driver, electron_app, ux_guard): _, pid, base_url = electron_app @@ -103,11 +108,14 @@ def test_type_text(self, driver, electron_app, ux_guard): try: driver.click(pid, wid, element_index=idx) time.sleep(0.4) - driver.call_tool("type_text_chars", { - "pid": pid, - "text": "hello electron", - "delay_ms": 30, - }) + driver.call_tool( + "type_text_chars", + { + "pid": pid, + "text": "hello electron", + "delay_ms": 30, + }, + ) except Exception as e: events = _http_events(base_url) key_events = [ev for ev in events if ev.get("type") in ("keydown", "keypress", "input")] @@ -135,7 +143,9 @@ def test_type_text(self, driver, electron_app, ux_guard): if after is None or not after.tree: pytest.skip("AX tree unavailable after typing (Electron AX rebuild timeout)") - print(f"\n after tree length: {len(after.tree)}, has text: {after.has_text('hello electron')}") + print( + f"\n after tree length: {len(after.tree)}, has text: {after.has_text('hello electron')}" + ) assert after.has_text("hello electron"), f"Text not in AX tree:\n{after.tree[:500]}" @@ -162,6 +172,7 @@ def test_canvas_pixel_click_no_focus_steal(self, driver, electron_app, ux_guard) assert mid.screenshot_b64, "No screenshot after 5 retries" from harness.cv import decode, diff_ratio + before_img = decode(mid.screenshot_b64) x = mid.screenshot_width // 2 diff --git a/libs/cua-driver/Tests/integration/test_hermes_chrome_form_fill.py b/libs/cua-driver/Tests/integration/test_hermes_chrome_form_fill.py index 2ddff05cc8..bd2d75a8e3 100644 --- a/libs/cua-driver/Tests/integration/test_hermes_chrome_form_fill.py +++ b/libs/cua-driver/Tests/integration/test_hermes_chrome_form_fill.py @@ -61,7 +61,11 @@ import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from driver_client import DriverClient, default_binary_path, resolve_window_id # noqa: E402 +from driver_client import ( # noqa: E402 + DriverClient, + default_binary_path, + resolve_window_id, +) # --------------------------------------------------------------------------- # Paths & constants @@ -77,8 +81,8 @@ _FOCUS_APP_EXE = os.path.join( _FOCUS_APP_DIR, "FocusMonitorApp.app", "Contents", "MacOS", "FocusMonitorApp" ) -_LOSS_FILE = "/tmp/focus_monitor_losses.txt" -_KEY_LOSS_FILE = "/tmp/focus_monitor_key_losses.txt" +_LOSS_FILE = "/tmp/focus_monitor_losses.txt" +_KEY_LOSS_FILE = "/tmp/focus_monitor_key_losses.txt" _FIELD_LOSS_FILE = "/tmp/focus_monitor_field_losses.txt" _HERMES_BIN = os.path.expanduser("~/.hermes/hermes-agent/venv/bin/hermes") @@ -95,6 +99,7 @@ # Chrome Apple Events JS helpers # --------------------------------------------------------------------------- + def _enable_chrome_apple_events() -> None: """Quit Chrome, write allow_javascript_apple_events to all profiles. @@ -105,14 +110,13 @@ def _enable_chrome_apple_events() -> None: """ subprocess.run( ["osascript", "-e", 'quit app "Google Chrome"'], - check=False, timeout=10, + check=False, + timeout=10, ) time.sleep(1.5) for prefs_path in glob.glob( - os.path.expanduser( - "~/Library/Application Support/Google/Chrome/*/Preferences" - ) + os.path.expanduser("~/Library/Application Support/Google/Chrome/*/Preferences") ): profile = prefs_path.split("/")[-2] if "System" in profile or "Guest" in profile: @@ -141,12 +145,10 @@ def _js_chrome(expr: str) -> str: # AppleScript: tell application "Google Chrome" to execute front window's # active tab javascript "" osa = ( - "tell application \"Google Chrome\" to execute " + 'tell application "Google Chrome" to execute ' f"front window's active tab javascript {json.dumps(expr)}" ) - r = subprocess.run( - ["osascript", "-e", osa], capture_output=True, text=True, timeout=15 - ) + r = subprocess.run(["osascript", "-e", osa], capture_output=True, text=True, timeout=15) return r.stdout.strip() @@ -168,16 +170,16 @@ def _wait_for_chrome_form(timeout: float = 30.0) -> bool: # Map logical field names → HTML element IDs in form_all_inputs.html. _FIELD_TO_ID: dict[str, str] = { - "text": "f-text", - "email": "f-email", + "text": "f-text", + "email": "f-email", "password": "f-password", - "number": "f-number", - "tel": "f-tel", + "number": "f-number", + "tel": "f-tel", "textarea": "f-textarea", - "select": "f-select", - "range": "f-range", - "date": "f-date", - "color": "f-color", + "select": "f-select", + "range": "f-range", + "date": "f-date", + "color": "f-color", } @@ -208,9 +210,13 @@ def _checkbox_checked() -> bool: # FocusMonitorApp helpers # --------------------------------------------------------------------------- + def _launch_focus_app() -> tuple[subprocess.Popen, int]: proc = subprocess.Popen( - [_FOCUS_APP_EXE], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, + [_FOCUS_APP_EXE], + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, ) for _ in range(40): line = proc.stdout.readline().strip() @@ -233,6 +239,7 @@ def _read_file_int(path: str) -> int: # Base test class # --------------------------------------------------------------------------- + class _HermesChromFormBase(unittest.TestCase): """Shared setup/teardown and hermes runner for all Chrome per-input tests.""" @@ -248,9 +255,7 @@ def setUpClass(cls) -> None: "ANTHROPIC_API_KEY is not set — skipping hermes Chrome form-fill tests" ) if not os.path.exists(_HERMES_BIN): - raise unittest.SkipTest( - f"hermes not found at {_HERMES_BIN} — install hermes first" - ) + raise unittest.SkipTest(f"hermes not found at {_HERMES_BIN} — install hermes first") for f in (_LOSS_FILE, _KEY_LOSS_FILE, _FIELD_LOSS_FILE): try: @@ -260,8 +265,9 @@ def setUpClass(cls) -> None: # Rebuild FocusMonitorApp when Swift source is newer. src = os.path.join(_FOCUS_APP_DIR, "FocusMonitorApp.swift") - if (not os.path.exists(_FOCUS_APP_EXE) - or os.path.getmtime(src) > os.path.getmtime(_FOCUS_APP_EXE)): + if not os.path.exists(_FOCUS_APP_EXE) or os.path.getmtime(src) > os.path.getmtime( + _FOCUS_APP_EXE + ): subprocess.run([os.path.join(_FOCUS_APP_DIR, "build.sh")], check=True) cls.binary = default_binary_path() @@ -275,10 +281,13 @@ def setUpClass(cls) -> None: # launch_app — no activation, no window raise, no focus steal. print(f" Launching Chrome hidden with {_FORM_URL}") with DriverClient(cls.binary) as c: - result = c.call_tool("launch_app", { - "bundle_id": CHROME_BUNDLE, - "urls": [_FORM_URL], - }) + result = c.call_tool( + "launch_app", + { + "bundle_id": CHROME_BUNDLE, + "urls": [_FORM_URL], + }, + ) sc = result.get("structuredContent", {}) cls._chrome_pid = sc.get("pid", 0) if not cls._chrome_pid: @@ -310,13 +319,15 @@ def setUpClass(cls) -> None: # baseline in setUp(), so startup-phase transients are not counted # as test failures. prev = _read_file_int(_FIELD_LOSS_FILE) - for _ in range(20): # up to 10 s + for _ in range(20): # up to 10 s time.sleep(0.5) curr = _read_file_int(_FIELD_LOSS_FILE) if curr == prev: break prev = curr - print(f" FocusMonitorApp ready (field_losses baseline: {_read_file_int(_FIELD_LOSS_FILE)})") + print( + f" FocusMonitorApp ready (field_losses baseline: {_read_file_int(_FIELD_LOSS_FILE)})" + ) @classmethod def tearDownClass(cls) -> None: @@ -327,12 +338,13 @@ def tearDownClass(cls) -> None: cls._focus_proc.kill() subprocess.run( ["osascript", "-e", 'quit app "Google Chrome"'], - check=False, timeout=10, + check=False, + timeout=10, ) def setUp(self) -> None: - self._losses_before = _read_file_int(_LOSS_FILE) - self._key_losses_before = _read_file_int(_KEY_LOSS_FILE) + self._losses_before = _read_file_int(_LOSS_FILE) + self._key_losses_before = _read_file_int(_KEY_LOSS_FILE) self._field_losses_before = _read_file_int(_FIELD_LOSS_FILE) def _run_hermes(self, prompt: str, timeout: int = 120, max_turns: int = 5) -> str: @@ -344,16 +356,25 @@ def _run_hermes(self, prompt: str, timeout: int = 120, max_turns: int = 5) -> st result = subprocess.run( [ - _HERMES_BIN, "chat", - "--provider", "anthropic", - "-m", _MODEL, - "--toolsets", "computer_use", + _HERMES_BIN, + "chat", + "--provider", + "anthropic", + "-m", + _MODEL, + "--toolsets", + "computer_use", "--yolo", "-Q", - "--max-turns", str(max_turns), - "-q", prompt, + "--max-turns", + str(max_turns), + "-q", + prompt, ], - env=env, capture_output=True, text=True, timeout=timeout, + env=env, + capture_output=True, + text=True, + timeout=timeout, ) return (result.stdout + "\n" + result.stderr).strip() @@ -371,8 +392,8 @@ def _assert_no_new_focus_loss(self, allow_field_losses: int = 0) -> None: value for multi-action tests. """ time.sleep(0.3) - app_delta = _read_file_int(_LOSS_FILE) - self._losses_before - key_delta = _read_file_int(_KEY_LOSS_FILE) - self._key_losses_before + app_delta = _read_file_int(_LOSS_FILE) - self._losses_before + key_delta = _read_file_int(_KEY_LOSS_FILE) - self._key_losses_before field_delta = _read_file_int(_FIELD_LOSS_FILE) - self._field_losses_before msgs = [] if app_delta: @@ -385,7 +406,8 @@ def _assert_no_new_focus_loss(self, allow_field_losses: int = 0) -> None: f" (allowed: {allow_field_losses})" ) self.assertEqual( - len(msgs), 0, + len(msgs), + 0, f"Focus stolen in {self._testMethodName}: " + "; ".join(msgs) + " — background CGEvent delivery must not steal focus", @@ -405,11 +427,14 @@ def _ax_contains(self, query: str) -> bool: try: wid = self._chrome_window_id() with DriverClient(self.binary) as c: - snap = c.call_tool("get_window_state", { - "pid": self._chrome_pid, - "window_id": wid, - "query": query, - }) + snap = c.call_tool( + "get_window_state", + { + "pid": self._chrome_pid, + "window_id": wid, + "query": query, + }, + ) tree = snap.get("structuredContent", snap).get("tree_markdown", "") return len(tree.strip()) > 0 except Exception: @@ -420,6 +445,7 @@ def _ax_contains(self, query: str) -> bool: # One test class per input type # --------------------------------------------------------------------------- + class TestChromeTextInput(_HermesChromFormBase): """ in Chrome — type_text CGEvent fallback path.""" @@ -431,8 +457,7 @@ def test_text_input(self) -> None: "then type 'Hello Chrome' into it. Stop after typing." ) val = _field("text") - self.assertEqual(val, "Hello Chrome", - msg=f"text='{val}'\nhermes: {out[-400:]}") + self.assertEqual(val, "Hello Chrome", msg=f"text='{val}'\nhermes: {out[-400:]}") self._assert_no_new_focus_loss(allow_field_losses=3) @@ -447,8 +472,7 @@ def test_email_input(self) -> None: "Stop after typing." ) val = _field("email") - self.assertEqual(val, "agent@chrome.com", - msg=f"email='{val}'\nhermes: {out[-400:]}") + self.assertEqual(val, "agent@chrome.com", msg=f"email='{val}'\nhermes: {out[-400:]}") self._assert_no_new_focus_loss(allow_field_losses=3) @@ -463,8 +487,7 @@ def test_password_input(self) -> None: "Stop after typing." ) val = _field("password") - self.assertEqual(val, "Secure123!", - msg=f"password='{val}'\nhermes: {out[-400:]}") + self.assertEqual(val, "Secure123!", msg=f"password='{val}'\nhermes: {out[-400:]}") self._assert_no_new_focus_loss(allow_field_losses=3) @@ -479,8 +502,7 @@ def test_number_input(self) -> None: "Stop after typing." ) val = _field("number") - self.assertEqual(val, "99", - msg=f"number='{val}'\nhermes: {out[-400:]}") + self.assertEqual(val, "99", msg=f"number='{val}'\nhermes: {out[-400:]}") self._assert_no_new_focus_loss(allow_field_losses=3) @@ -495,8 +517,7 @@ def test_textarea(self) -> None: "Stop after typing." ) val = _field("textarea") - self.assertIn("chrome bg ok", val, - msg=f"textarea='{val}'\nhermes: {out[-400:]}") + self.assertIn("chrome bg ok", val, msg=f"textarea='{val}'\nhermes: {out[-400:]}") self._assert_no_new_focus_loss(allow_field_losses=3) @@ -510,8 +531,7 @@ def test_checkbox(self) -> None: "Click the checkbox labelled 'I agree to the terms' (id='f-checkbox'). " "Stop after clicking." ) - self.assertTrue(_checkbox_checked(), - msg=f"checkbox not checked\nhermes: {out[-400:]}") + self.assertTrue(_checkbox_checked(), msg=f"checkbox not checked\nhermes: {out[-400:]}") self._assert_no_new_focus_loss(allow_field_losses=3) @@ -534,8 +554,7 @@ def test_select_dropdown(self) -> None: timeout=180, ) val = _field("select") - self.assertEqual(val, "blue", - msg=f"select='{val}'\nhermes: {out[-400:]}") + self.assertEqual(val, "blue", msg=f"select='{val}'\nhermes: {out[-400:]}") self._assert_no_new_focus_loss(allow_field_losses=3) @@ -556,8 +575,7 @@ def test_submit_button(self) -> None: '(function(){var e=document.getElementById("result");' 'return (e && e.textContent.trim().length > 0) ? "true" : "false";})()' ) - self.assertEqual(submitted, "true", - msg=f"form not submitted\nhermes: {out[-400:]}") + self.assertEqual(submitted, "true", msg=f"form not submitted\nhermes: {out[-400:]}") self._assert_no_new_focus_loss(allow_field_losses=3) @@ -585,17 +603,17 @@ def test_full_form_flow(self) -> None: timeout=360, ) # Verify all three fields were written. - text_val = _field("text") + text_val = _field("text") email_val = _field("email") - num_val = _field("number") + num_val = _field("number") errors: list[str] = [] - if text_val != "E2E Test": errors.append(f"text='{text_val}'") - if email_val != "e2e@test.com": errors.append(f"email='{email_val}'") - if num_val != "7": errors.append(f"number='{num_val}'") - self.assertFalse( - errors, - f"Field mismatch: {'; '.join(errors)}\nhermes: {out[-600:]}" - ) + if text_val != "E2E Test": + errors.append(f"text='{text_val}'") + if email_val != "e2e@test.com": + errors.append(f"email='{email_val}'") + if num_val != "7": + errors.append(f"number='{num_val}'") + self.assertFalse(errors, f"Field mismatch: {'; '.join(errors)}\nhermes: {out[-600:]}") self._assert_no_new_focus_loss(allow_field_losses=3) diff --git a/libs/cua-driver/Tests/integration/test_hermes_form_fill.py b/libs/cua-driver/Tests/integration/test_hermes_form_fill.py index bf7b1c873c..c9933e48fe 100644 --- a/libs/cua-driver/Tests/integration/test_hermes_form_fill.py +++ b/libs/cua-driver/Tests/integration/test_hermes_form_fill.py @@ -52,8 +52,8 @@ _FOCUS_APP_EXE = os.path.join( _FOCUS_APP_DIR, "FocusMonitorApp.app", "Contents", "MacOS", "FocusMonitorApp" ) -_LOSS_FILE = "/tmp/focus_monitor_losses.txt" -_KEY_LOSS_FILE = "/tmp/focus_monitor_key_losses.txt" +_LOSS_FILE = "/tmp/focus_monitor_losses.txt" +_KEY_LOSS_FILE = "/tmp/focus_monitor_key_losses.txt" _FIELD_LOSS_FILE = "/tmp/focus_monitor_field_losses.txt" _HERMES_BIN = os.path.expanduser("~/.hermes/hermes-agent/venv/bin/hermes") @@ -71,6 +71,7 @@ # Helpers # --------------------------------------------------------------------------- + def _build_focus_app() -> None: if not os.path.exists(_FOCUS_APP_EXE): subprocess.run([os.path.join(_FOCUS_APP_DIR, "build.sh")], check=True) @@ -78,7 +79,10 @@ def _build_focus_app() -> None: def _launch_focus_app() -> tuple[subprocess.Popen, int]: proc = subprocess.Popen( - [_FOCUS_APP_EXE], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, + [_FOCUS_APP_EXE], + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, ) for _ in range(40): line = proc.stdout.readline().strip() @@ -145,6 +149,7 @@ def _checkbox_checked() -> bool: # Base test class # --------------------------------------------------------------------------- + class _HermesFormBase(unittest.TestCase): """Shared setup/teardown and the hermes runner for all per-input tests.""" @@ -164,8 +169,9 @@ def setUpClass(cls) -> None: # Force rebuild FocusMonitorApp when Swift source is newer. src = os.path.join(_FOCUS_APP_DIR, "FocusMonitorApp.swift") - if (not os.path.exists(_FOCUS_APP_EXE) - or os.path.getmtime(src) > os.path.getmtime(_FOCUS_APP_EXE)): + if not os.path.exists(_FOCUS_APP_EXE) or os.path.getmtime(src) > os.path.getmtime( + _FOCUS_APP_EXE + ): subprocess.run([os.path.join(_FOCUS_APP_DIR, "build.sh")], check=True) subprocess.run(["pkill", "-x", "Safari"], check=False) @@ -189,8 +195,8 @@ def tearDownClass(cls) -> None: subprocess.run(["pkill", "-x", "Safari"], check=False) def setUp(self) -> None: - self._losses_before = _read_focus_losses() - self._key_losses_before = _read_key_losses() + self._losses_before = _read_focus_losses() + self._key_losses_before = _read_key_losses() self._field_losses_before = _read_field_losses() def _run_hermes(self, prompt: str, timeout: int = 120, max_turns: int = 5) -> str: @@ -202,25 +208,34 @@ def _run_hermes(self, prompt: str, timeout: int = 120, max_turns: int = 5) -> st result = subprocess.run( [ - _HERMES_BIN, "chat", - "--provider", "anthropic", - "-m", _MODEL, - "--toolsets", "computer_use", - "--yolo", # auto-approve every computer_use action - "-Q", # quiet: only final response + session line - "--max-turns", str(max_turns), - "-q", prompt, + _HERMES_BIN, + "chat", + "--provider", + "anthropic", + "-m", + _MODEL, + "--toolsets", + "computer_use", + "--yolo", # auto-approve every computer_use action + "-Q", # quiet: only final response + session line + "--max-turns", + str(max_turns), + "-q", + prompt, ], - env=env, capture_output=True, text=True, timeout=timeout, + env=env, + capture_output=True, + text=True, + timeout=timeout, ) return (result.stdout + "\n" + result.stderr).strip() def _assert_no_new_focus_loss(self) -> None: """Assert no new app-level OR key-level focus losses occurred.""" time.sleep(0.3) # let any async activation settle - app_delta = _read_focus_losses() - self._losses_before - key_delta = _read_key_losses() - self._key_losses_before - field_delta = _read_field_losses() - self._field_losses_before + app_delta = _read_focus_losses() - self._losses_before + key_delta = _read_key_losses() - self._key_losses_before + field_delta = _read_field_losses() - self._field_losses_before msgs = [] if app_delta: msgs.append(f"app lost focus {app_delta}x") @@ -229,7 +244,8 @@ def _assert_no_new_focus_loss(self) -> None: if field_delta: msgs.append(f"text-input lost first-responder {field_delta}x") self.assertEqual( - len(msgs), 0, + len(msgs), + 0, f"Focus stolen in {self._testMethodName}: " + "; ".join(msgs) + " — background events must not steal focus", @@ -240,6 +256,7 @@ def _assert_no_new_focus_loss(self) -> None: # One test class per input type # --------------------------------------------------------------------------- + class TestTextInput(_HermesFormBase): """ — fill with a short string.""" @@ -251,8 +268,7 @@ def test_text_input(self) -> None: "then type 'Hello World' into it. Stop after typing." ) val = _field("text") - self.assertEqual(val, "Hello World", - msg=f"text='{val}'\nhermes: {out[-400:]}") + self.assertEqual(val, "Hello World", msg=f"text='{val}'\nhermes: {out[-400:]}") self._assert_no_new_focus_loss() @@ -267,8 +283,7 @@ def test_password_input(self) -> None: "Stop after typing." ) val = _field("password") - self.assertEqual(val, "Pass1234!", - msg=f"password='{val}'\nhermes: {out[-400:]}") + self.assertEqual(val, "Pass1234!", msg=f"password='{val}'\nhermes: {out[-400:]}") self._assert_no_new_focus_loss() @@ -283,8 +298,7 @@ def test_email_input(self) -> None: "Stop after typing." ) val = _field("email") - self.assertEqual(val, "agent@example.com", - msg=f"email='{val}'\nhermes: {out[-400:]}") + self.assertEqual(val, "agent@example.com", msg=f"email='{val}'\nhermes: {out[-400:]}") self._assert_no_new_focus_loss() @@ -299,8 +313,7 @@ def test_number_input(self) -> None: "Stop after typing." ) val = _field("number") - self.assertEqual(val, "42", - msg=f"number='{val}'\nhermes: {out[-400:]}") + self.assertEqual(val, "42", msg=f"number='{val}'\nhermes: {out[-400:]}") self._assert_no_new_focus_loss() @@ -315,8 +328,7 @@ def test_textarea(self) -> None: "Stop after typing." ) val = _field("textarea") - self.assertIn("bg works", val, - msg=f"textarea='{val}'\nhermes: {out[-400:]}") + self.assertIn("bg works", val, msg=f"textarea='{val}'\nhermes: {out[-400:]}") self._assert_no_new_focus_loss() @@ -330,8 +342,7 @@ def test_checkbox(self) -> None: "Then AX-click the checkbox labelled 'I agree to the terms' (id='f-checkbox'). " "Stop after clicking." ) - self.assertTrue(_checkbox_checked(), - msg=f"checkbox not checked\nhermes: {out[-400:]}") + self.assertTrue(_checkbox_checked(), msg=f"checkbox not checked\nhermes: {out[-400:]}") self._assert_no_new_focus_loss() @@ -355,8 +366,7 @@ def test_select_dropdown(self) -> None: timeout=180, ) val = _field("select") - self.assertEqual(val, "blue", - msg=f"select='{val}'\nhermes: {out[-400:]}") + self.assertEqual(val, "blue", msg=f"select='{val}'\nhermes: {out[-400:]}") self._assert_no_new_focus_loss() @@ -371,8 +381,7 @@ def test_radio_button(self) -> None: "Stop after clicking." ) val = _field("radio") - self.assertEqual(val, "banana", - msg=f"radio='{val}'\nhermes: {out[-400:]}") + self.assertEqual(val, "banana", msg=f"radio='{val}'\nhermes: {out[-400:]}") self._assert_no_new_focus_loss() @@ -387,8 +396,7 @@ def test_submit_button(self) -> None: "Click it using its element index. Stop after clicking." ) submitted = _js("(window._submitted !== undefined).toString()") - self.assertEqual(submitted, "true", - msg=f"form not submitted\nhermes: {out[-400:]}") + self.assertEqual(submitted, "true", msg=f"form not submitted\nhermes: {out[-400:]}") self._assert_no_new_focus_loss() diff --git a/libs/cua-driver/Tests/integration/test_hermes_form_fill_qwen.py b/libs/cua-driver/Tests/integration/test_hermes_form_fill_qwen.py index 288a36f4ff..9dea3b5086 100644 --- a/libs/cua-driver/Tests/integration/test_hermes_form_fill_qwen.py +++ b/libs/cua-driver/Tests/integration/test_hermes_form_fill_qwen.py @@ -39,8 +39,8 @@ _FOCUS_APP_EXE = os.path.join( _FOCUS_APP_DIR, "FocusMonitorApp.app", "Contents", "MacOS", "FocusMonitorApp" ) -_LOSS_FILE = "/tmp/focus_monitor_losses.txt" -_KEY_LOSS_FILE = "/tmp/focus_monitor_key_losses.txt" +_LOSS_FILE = "/tmp/focus_monitor_losses.txt" +_KEY_LOSS_FILE = "/tmp/focus_monitor_key_losses.txt" _FIELD_LOSS_FILE = "/tmp/focus_monitor_field_losses.txt" _HERMES_BIN = os.path.expanduser("~/.hermes/hermes-agent/venv/bin/hermes") @@ -55,9 +55,13 @@ # Helpers (identical to test_hermes_form_fill.py) # --------------------------------------------------------------------------- + def _launch_focus_app() -> tuple[subprocess.Popen, int]: proc = subprocess.Popen( - [_FOCUS_APP_EXE], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, + [_FOCUS_APP_EXE], + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, ) for _ in range(40): line = proc.stdout.readline().strip() @@ -103,6 +107,7 @@ def _field(name: str) -> str: # Base test class # --------------------------------------------------------------------------- + class _QwenFormBase(unittest.TestCase): """Shared setup/teardown for Qwen-backed hermes form-fill tests.""" @@ -112,7 +117,9 @@ class _QwenFormBase(unittest.TestCase): @classmethod def setUpClass(cls) -> None: # Verify the local server is reachable before spending time on setup. - import urllib.request, urllib.error + import urllib.error + import urllib.request + try: urllib.request.urlopen(_ENDPOINT.rstrip("/") + "/models", timeout=3) except urllib.error.URLError as e: @@ -126,8 +133,9 @@ def setUpClass(cls) -> None: os.remove(f) src = os.path.join(_FOCUS_APP_DIR, "FocusMonitorApp.swift") - if (not os.path.exists(_FOCUS_APP_EXE) - or os.path.getmtime(src) > os.path.getmtime(_FOCUS_APP_EXE)): + if not os.path.exists(_FOCUS_APP_EXE) or os.path.getmtime(src) > os.path.getmtime( + _FOCUS_APP_EXE + ): subprocess.run([os.path.join(_FOCUS_APP_DIR, "build.sh")], check=True) subprocess.run(["pkill", "-x", "Safari"], check=False) @@ -146,8 +154,8 @@ def tearDownClass(cls) -> None: subprocess.run(["pkill", "-x", "Safari"], check=False) def setUp(self) -> None: - self._losses_before = _read_file_int(_LOSS_FILE) - self._key_losses_before = _read_file_int(_KEY_LOSS_FILE) + self._losses_before = _read_file_int(_LOSS_FILE) + self._key_losses_before = _read_file_int(_KEY_LOSS_FILE) self._field_losses_before = _read_file_int(_FIELD_LOSS_FILE) def _run_hermes(self, prompt: str, timeout: int = 180, max_turns: int = 6) -> str: @@ -159,34 +167,46 @@ def _run_hermes(self, prompt: str, timeout: int = 180, max_turns: int = 6) -> st # (provider: custom, base_url: http://127.0.0.1:8080/v1, model: Qwen3.6…) result = subprocess.run( [ - _HERMES_BIN, "chat", - "--toolsets", "computer_use", + _HERMES_BIN, + "chat", + "--toolsets", + "computer_use", "--yolo", "-Q", - "--max-turns", str(max_turns), - "-q", prompt, + "--max-turns", + str(max_turns), + "-q", + prompt, ], - env=env, capture_output=True, text=True, timeout=timeout, + env=env, + capture_output=True, + text=True, + timeout=timeout, ) return (result.stdout + "\n" + result.stderr).strip() def _assert_no_new_focus_loss(self) -> None: time.sleep(0.3) - app_delta = _read_file_int(_LOSS_FILE) - self._losses_before - key_delta = _read_file_int(_KEY_LOSS_FILE) - self._key_losses_before + app_delta = _read_file_int(_LOSS_FILE) - self._losses_before + key_delta = _read_file_int(_KEY_LOSS_FILE) - self._key_losses_before field_delta = _read_file_int(_FIELD_LOSS_FILE) - self._field_losses_before msgs = [] - if app_delta: msgs.append(f"app lost focus {app_delta}x") - if key_delta: msgs.append(f"window lost key status {key_delta}x") - if field_delta: msgs.append(f"text-input lost first-responder {field_delta}x") - self.assertEqual(len(msgs), 0, - f"Focus stolen in {self._testMethodName}: " + "; ".join(msgs)) + if app_delta: + msgs.append(f"app lost focus {app_delta}x") + if key_delta: + msgs.append(f"window lost key status {key_delta}x") + if field_delta: + msgs.append(f"text-input lost first-responder {field_delta}x") + self.assertEqual( + len(msgs), 0, f"Focus stolen in {self._testMethodName}: " + "; ".join(msgs) + ) # --------------------------------------------------------------------------- # Tests — start with text input only; add more once text passes # --------------------------------------------------------------------------- + class TestQwenTextInput(_QwenFormBase): """ — verifies multimodal tool results work with mlx-vlm.""" @@ -198,8 +218,7 @@ def test_text_input(self) -> None: "then type 'Hello World' into it. Stop after typing." ) val = _field("text") - self.assertEqual(val, "Hello World", - msg=f"text='{val}'\nhermes: {out[-600:]}") + self.assertEqual(val, "Hello World", msg=f"text='{val}'\nhermes: {out[-600:]}") self._assert_no_new_focus_loss() @@ -214,8 +233,7 @@ def test_email_input(self) -> None: "Stop after typing." ) val = _field("email") - self.assertEqual(val, "agent@example.com", - msg=f"email='{val}'\nhermes: {out[-600:]}") + self.assertEqual(val, "agent@example.com", msg=f"email='{val}'\nhermes: {out[-600:]}") self._assert_no_new_focus_loss() diff --git a/libs/cua-driver/Tests/integration/test_hermes_launch_safari_bg.py b/libs/cua-driver/Tests/integration/test_hermes_launch_safari_bg.py index 8239deb616..1763d0e7f1 100644 --- a/libs/cua-driver/Tests/integration/test_hermes_launch_safari_bg.py +++ b/libs/cua-driver/Tests/integration/test_hermes_launch_safari_bg.py @@ -50,8 +50,8 @@ _FOCUS_APP_EXE = os.path.join( _FOCUS_APP_DIR, "FocusMonitorApp.app", "Contents", "MacOS", "FocusMonitorApp" ) -_LOSS_FILE = "/tmp/focus_monitor_losses.txt" -_KEY_LOSS_FILE = "/tmp/focus_monitor_key_losses.txt" +_LOSS_FILE = "/tmp/focus_monitor_losses.txt" +_KEY_LOSS_FILE = "/tmp/focus_monitor_key_losses.txt" _FIELD_LOSS_FILE = "/tmp/focus_monitor_field_losses.txt" _HERMES_BIN = os.path.expanduser("~/.hermes/hermes-agent/venv/bin/hermes") @@ -60,12 +60,13 @@ _MODEL = os.environ.get("HERMES_TEST_MODEL", "claude-haiku-4-5-20251001") _SAFARI_BUNDLE = "com.apple.Safari" -_LAUNCH_URL = "https://example.com" +_LAUNCH_URL = "https://example.com" # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- + def _read_file_int(path: str) -> int: try: with open(path) as f: @@ -93,9 +94,7 @@ def _safari_window_count_via_driver(wait_s: float = 4.0) -> int: r = c.call_tool("list_windows", {}) sc = r.get("structuredContent") or {} wins = sc.get("windows") or [] - count = sum( - 1 for w in wins if "safari" in w.get("app_name", "").lower() - ) + count = sum(1 for w in wins if "safari" in w.get("app_name", "").lower()) if count > 0 or time.time() >= deadline: return count time.sleep(0.5) @@ -103,7 +102,10 @@ def _safari_window_count_via_driver(wait_s: float = 4.0) -> int: def _launch_focus_app() -> tuple[subprocess.Popen, int]: proc = subprocess.Popen( - [_FOCUS_APP_EXE], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, + [_FOCUS_APP_EXE], + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, ) for _ in range(40): line = proc.stdout.readline().strip() @@ -118,6 +120,7 @@ def _launch_focus_app() -> tuple[subprocess.Popen, int]: # Test # --------------------------------------------------------------------------- + class TestHermesLaunchSafariBg(unittest.TestCase): """Verify hermes launches Safari in the background without stealing focus.""" @@ -133,8 +136,9 @@ def setUpClass(cls) -> None: # Build FocusMonitorApp if needed. src = os.path.join(_FOCUS_APP_DIR, "FocusMonitorApp.swift") - if (not os.path.exists(_FOCUS_APP_EXE) - or os.path.getmtime(src) > os.path.getmtime(_FOCUS_APP_EXE)): + if not os.path.exists(_FOCUS_APP_EXE) or os.path.getmtime(src) > os.path.getmtime( + _FOCUS_APP_EXE + ): subprocess.run([os.path.join(_FOCUS_APP_DIR, "build.sh")], check=True) # Kill Safari so we can detect a fresh launch. @@ -159,8 +163,8 @@ def setUpClass(cls) -> None: prev = cur time.sleep(0.3) - cls._losses_before = _read_file_int(_LOSS_FILE) - cls._key_losses_before = _read_file_int(_KEY_LOSS_FILE) + cls._losses_before = _read_file_int(_LOSS_FILE) + cls._key_losses_before = _read_file_int(_KEY_LOSS_FILE) cls._field_losses_before = _read_file_int(_FIELD_LOSS_FILE) @classmethod @@ -190,23 +194,32 @@ def _run_hermes(self, prompt: str, timeout: int = 240, max_turns: int = 8) -> st result = subprocess.run( [ - _HERMES_BIN, "chat", - "--provider", "anthropic", - "-m", _MODEL, - "--toolsets", "computer_use", + _HERMES_BIN, + "chat", + "--provider", + "anthropic", + "-m", + _MODEL, + "--toolsets", + "computer_use", "--yolo", "-Q", - "--max-turns", str(max_turns), - "-q", prompt, + "--max-turns", + str(max_turns), + "-q", + prompt, ], - env=env, capture_output=True, text=True, timeout=timeout, + env=env, + capture_output=True, + text=True, + timeout=timeout, ) return (result.stdout + "\n" + result.stderr).strip() def _assert_no_new_focus_loss(self, allow_field_losses: int = 1) -> None: time.sleep(0.3) - app_delta = _read_file_int(_LOSS_FILE) - self._losses_before - key_delta = _read_file_int(_KEY_LOSS_FILE) - self._key_losses_before + app_delta = _read_file_int(_LOSS_FILE) - self._losses_before + key_delta = _read_file_int(_KEY_LOSS_FILE) - self._key_losses_before field_delta = _read_file_int(_FIELD_LOSS_FILE) - self._field_losses_before msgs = [] if app_delta: @@ -215,11 +228,11 @@ def _assert_no_new_focus_loss(self, allow_field_losses: int = 1) -> None: msgs.append(f"window lost key status {key_delta}x") if field_delta > allow_field_losses: msgs.append( - f"text-input lost first-responder {field_delta}x " - f"(allowed {allow_field_losses})" + f"text-input lost first-responder {field_delta}x " f"(allowed {allow_field_losses})" ) self.assertEqual( - len(msgs), 0, + len(msgs), + 0, "Focus stolen during launch_app: " + "; ".join(msgs), ) @@ -256,7 +269,8 @@ def test_launch_safari_background_no_focus_steal(self) -> None: # the URL loads, so _safari_window_count_via_driver polls for up to 4s. window_count = _safari_window_count_via_driver(wait_s=4.0) self.assertGreater( - window_count, 0, + window_count, + 0, f"cua-driver sees no Safari windows after launch_app+URL " f"(got {window_count}). " f"Safari may have launched without a window — ensure urls=['{_LAUNCH_URL}'] " diff --git a/libs/cua-driver/Tests/integration/test_hidden_app_capture.py b/libs/cua-driver/Tests/integration/test_hidden_app_capture.py index 962c614b47..b7d7331b99 100644 --- a/libs/cua-driver/Tests/integration/test_hidden_app_capture.py +++ b/libs/cua-driver/Tests/integration/test_hidden_app_capture.py @@ -33,7 +33,6 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from driver_client import DriverClient, default_binary_path, reset_calculator - CALCULATOR_BUNDLE = "com.apple.calculator" @@ -52,12 +51,8 @@ def tearDown(self) -> None: def test_hidden_window_appears_in_list_windows(self) -> None: """launch_app → window appears in list_windows even if off-screen.""" - result = self.client.call_tool( - "launch_app", {"bundle_id": CALCULATOR_BUNDLE} - ) - self.assertFalse( - result.get("isError"), f"launch_app failed: {result}" - ) + result = self.client.call_tool("launch_app", {"bundle_id": CALCULATOR_BUNDLE}) + self.assertFalse(result.get("isError"), f"launch_app failed: {result}") pid = result["structuredContent"]["pid"] self.assertIsInstance(pid, int) self.assertGreater(pid, 0) @@ -66,9 +61,7 @@ def test_hidden_window_appears_in_list_windows(self) -> None: time.sleep(1.0) # list_windows (no filter) must include Calculator's window. - all_windows = self.client.call_tool("list_windows", {})[ - "structuredContent" - ]["windows"] + all_windows = self.client.call_tool("list_windows", {})["structuredContent"]["windows"] calc_windows = [w for w in all_windows if w["pid"] == pid] self.assertGreater( len(calc_windows), @@ -84,20 +77,16 @@ def test_hidden_window_appears_in_list_windows(self) -> None: def test_screenshot_succeeds_for_hidden_window(self) -> None: """screenshot tool captures the backing store even when is_on_screen=false.""" - result = self.client.call_tool( - "launch_app", {"bundle_id": CALCULATOR_BUNDLE} - ) + result = self.client.call_tool("launch_app", {"bundle_id": CALCULATOR_BUNDLE}) self.assertFalse(result.get("isError"), f"launch_app failed: {result}") pid = result["structuredContent"]["pid"] time.sleep(1.0) # Grab a window_id for Calculator. - windows_result = self.client.call_tool("list_windows", {"pid": pid})[ - "structuredContent" - ]["windows"] - self.assertGreater( - len(windows_result), 0, "No windows found for Calculator" - ) + windows_result = self.client.call_tool("list_windows", {"pid": pid})["structuredContent"][ + "windows" + ] + self.assertGreater(len(windows_result), 0, "No windows found for Calculator") window_id = windows_result[0]["window_id"] # screenshot must succeed regardless of is_on_screen. diff --git a/libs/cua-driver/Tests/integration/test_launch_app_visible.py b/libs/cua-driver/Tests/integration/test_launch_app_visible.py index c9584a1bd7..ea29065e91 100644 --- a/libs/cua-driver/Tests/integration/test_launch_app_visible.py +++ b/libs/cua-driver/Tests/integration/test_launch_app_visible.py @@ -29,7 +29,8 @@ def _on_screen_windows(driver, pid: int) -> list[dict]: result = driver.call_tool("list_windows", {"pid": pid}) windows = result.get("structuredContent", {}).get("windows", []) return [ - w for w in windows + w + for w in windows if w.get("is_on_screen") and (w.get("bounds", {}).get("width", 0) or 0) > 50 and (w.get("bounds", {}).get("height", 0) or 0) > 50 @@ -39,12 +40,13 @@ def _on_screen_windows(driver, pid: int) -> list[dict]: def _pid_from_result(result: dict) -> int: """Extract pid from a launch_app result (structuredContent or text fallback).""" import re + pid = result.get("structuredContent", {}).get("pid", 0) if pid: return pid for item in result.get("content", []): text = item.get("text", "") - m = re.search(r'\bpid\s+(\d+)', text) + m = re.search(r"\bpid\s+(\d+)", text) if m: return int(m.group(1)) return 0 @@ -52,6 +54,7 @@ def _pid_from_result(result: dict) -> int: # ── module-scoped setup ─────────────────────────────────────────────────────── + @pytest.fixture(scope="module", autouse=True) def _kill_test_apps(): """Kill test apps before the module so each test cold-launches cleanly.""" @@ -66,6 +69,7 @@ def _kill_test_apps(): # ── tests ───────────────────────────────────────────────────────────────────── + def test_textedit_cold_launch_opens_visible_window(driver, focus_monitor, ux_guard): """launch_app(TextEdit) must open an on-screen window with 0 focus losses.""" result = driver.call_tool("launch_app", {"bundle_id": TEXTEDIT_BUNDLE}) diff --git a/libs/cua-driver/Tests/integration/test_list_windows.py b/libs/cua-driver/Tests/integration/test_list_windows.py index 65b272ede1..f52937c181 100644 --- a/libs/cua-driver/Tests/integration/test_list_windows.py +++ b/libs/cua-driver/Tests/integration/test_list_windows.py @@ -26,7 +26,6 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from driver_client import DriverClient, default_binary_path, reset_calculator - CALCULATOR_BUNDLE = "com.apple.calculator" @@ -37,9 +36,7 @@ def setUp(self) -> None: # Launch Calculator so we have at least one guaranteed window to # anchor the assertions on. launch_app is idempotent; it'll # return the pid whether or not Calculator was already running. - result = self.client.call_tool( - "launch_app", {"bundle_id": CALCULATOR_BUNDLE} - ) + result = self.client.call_tool("launch_app", {"bundle_id": CALCULATOR_BUNDLE}) self.calc_pid = result["structuredContent"]["pid"] time.sleep(0.5) @@ -47,22 +44,24 @@ def tearDown(self) -> None: self.client.__exit__(None, None, None) def _call_list_windows(self, **args): - return self.client.call_tool("list_windows", args)[ - "structuredContent" - ] + return self.client.call_tool("list_windows", args)["structuredContent"] def test_default_returns_layer_zero_windows_with_new_field_names(self): body = self._call_list_windows() windows = body["windows"] - self.assertGreater( - len(windows), 0, "expected at least Calculator's window" - ) + self.assertGreater(len(windows), 0, "expected at least Calculator's window") # Field contract — window_id / app_name / title are the user-facing # renames from WindowInfo.id / owner / name. required = { - "window_id", "pid", "app_name", "title", "bounds", - "layer", "z_index", "is_on_screen", + "window_id", + "pid", + "app_name", + "title", + "bounds", + "layer", + "z_index", + "is_on_screen", } for w in windows: self.assertTrue( @@ -80,9 +79,7 @@ def test_default_returns_layer_zero_windows_with_new_field_names(self): def test_pid_filter_narrows_to_one_pid(self): body = self._call_list_windows(pid=self.calc_pid) windows = body["windows"] - self.assertGreater( - len(windows), 0, "Calculator should have at least one window" - ) + self.assertGreater(len(windows), 0, "Calculator should have at least one window") self.assertTrue( all(w["pid"] == self.calc_pid for w in windows), "pid filter leaked other pids into the result", @@ -94,9 +91,7 @@ def test_pid_filter_narrows_to_one_pid(self): def test_on_screen_only_drops_off_screen_entries(self): everything = self._call_list_windows()["windows"] - only_visible = self._call_list_windows(on_screen_only=True)[ - "windows" - ] + only_visible = self._call_list_windows(on_screen_only=True)["windows"] # The visible subset should never exceed the full set. self.assertLessEqual(len(only_visible), len(everything)) diff --git a/libs/cua-driver/Tests/integration/test_overlay_z_order.py b/libs/cua-driver/Tests/integration/test_overlay_z_order.py index ea48239e92..f2c0ff9eeb 100644 --- a/libs/cua-driver/Tests/integration/test_overlay_z_order.py +++ b/libs/cua-driver/Tests/integration/test_overlay_z_order.py @@ -31,7 +31,11 @@ import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from driver_client import DriverClient, default_binary_path, resolve_window_id # noqa: E402 +from driver_client import ( # noqa: E402 + DriverClient, + default_binary_path, + resolve_window_id, +) _THIS_DIR = os.path.dirname(os.path.abspath(__file__)) _REPO_ROOT = os.path.dirname(os.path.dirname(_THIS_DIR)) @@ -122,10 +126,9 @@ def test_overlay_at_normal_level_above_target(self) -> None: # Windows above Calculator that are NOT the driver overlay. fg_wins_before = [ - w for w in all_before - if w["z_index"] > calc_z_before - and w["pid"] != driver_pid - and w.get("is_on_screen") + w + for w in all_before + if w["z_index"] > calc_z_before and w["pid"] != driver_pid and w.get("is_on_screen") ] # Trigger a pixel click to make the overlay appear and pin above Calculator. @@ -158,9 +161,7 @@ def test_overlay_at_normal_level_above_target(self) -> None: # ── Assertion 2: overlay is above the target ──────────────────────────── calc_wins_after = [w for w in all_after if w["pid"] == self.calc_pid] - calc_z_after = ( - max(w["z_index"] for w in calc_wins_after) if calc_wins_after else 0 - ) + calc_z_after = max(w["z_index"] for w in calc_wins_after) if calc_wins_after else 0 self.assertGreater( overlay_z, calc_z_after, diff --git a/libs/cua-driver/Tests/integration/test_safari.py b/libs/cua-driver/Tests/integration/test_safari.py index 160a33e3d7..2f75b6234f 100644 --- a/libs/cua-driver/Tests/integration/test_safari.py +++ b/libs/cua-driver/Tests/integration/test_safari.py @@ -13,31 +13,33 @@ from __future__ import annotations +import os +import re import subprocess import sys import time -import os -import re import pytest _HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, _HERE) -from harness.driver import Driver from harness import tree as Tree +from harness.driver import Driver def _parse_clicks(tree_text: str) -> int: """Extract the 'clicks: N' counter value from the AX tree.""" - m = re.search(r'clicks:\s*(\d+)', tree_text) + m = re.search(r"clicks:\s*(\d+)", tree_text) return int(m.group(1)) if m else 0 + SAFARI_BUNDLE = "com.apple.Safari" FOCUS_MONITOR_BUNDLE = "com.trycua.FocusMonitorApp" # ── module-level Safari setup ───────────────────────────────────────────────── + @pytest.fixture(scope="module") def safari_pid(binary, html_server, focus_monitor): """Open Safari to test_page.html; return its pid. @@ -68,8 +70,11 @@ def _reactivate_focus(focus_monitor): """Restore FocusMonitorApp as frontmost before each test (by pid).""" _, pid = focus_monitor subprocess.run( - ["osascript", "-e", - f'tell application "System Events" to set frontmost of (first process whose unix id is {pid}) to true'], + [ + "osascript", + "-e", + f'tell application "System Events" to set frontmost of (first process whose unix id is {pid}) to true', + ], check=False, ) time.sleep(0.4) @@ -77,6 +82,7 @@ def _reactivate_focus(focus_monitor): # ── helpers ─────────────────────────────────────────────────────────────────── + def _get_state(d: Driver, pid: int, safari_pid_fixture) -> "harness.driver.WindowState": wid = d.find_window(pid) return d.get_window_state(pid, wid) @@ -84,6 +90,7 @@ def _get_state(d: Driver, pid: int, safari_pid_fixture) -> "harness.driver.Windo # ── tests ───────────────────────────────────────────────────────────────────── + class TestSafariButton: """AX-click the 'Click Me' button without stealing focus.""" @@ -102,9 +109,9 @@ def test_click_increments_counter(self, driver, safari_pid, ux_guard): after = driver.get_window_state(safari_pid, wid) count_after = _parse_clicks(after.tree) print(f"\n click counter: {count_before} → {count_after}") - assert count_after == count_before + 1, ( - f"Counter did not increment: {count_before} → {count_after}" - ) + assert ( + count_after == count_before + 1 + ), f"Counter did not increment: {count_before} → {count_after}" def test_click_increments_twice(self, driver, safari_pid, ux_guard): wid = driver.find_window(safari_pid) @@ -123,9 +130,9 @@ def test_click_increments_twice(self, driver, safari_pid, ux_guard): after = driver.get_window_state(safari_pid, wid) count_after = _parse_clicks(after.tree) print(f"\n click counter: {count_before} → {count_after}") - assert count_after == count_before + 2, ( - f"Counter should have incremented by 2: {count_before} → {count_after}" - ) + assert ( + count_after == count_before + 2 + ), f"Counter should have incremented by 2: {count_before} → {count_after}" class TestSafariTextInput: @@ -150,9 +157,7 @@ def test_type_text_appears_in_ax_tree(self, driver, safari_pid, ux_guard): time.sleep(0.6) after = driver.get_window_state(safari_pid, wid) - assert after.has_text("hello safari"), ( - f"Typed text not visible in AX tree:\n{after.tree}" - ) + assert after.has_text("hello safari"), f"Typed text not visible in AX tree:\n{after.tree}" def test_type_text_with_type_text_tool(self, driver, safari_pid, ux_guard): """Use type_text (not type_text_chars) and verify result.""" @@ -172,9 +177,7 @@ def test_type_text_with_type_text_tool(self, driver, safari_pid, ux_guard): time.sleep(0.6) after = driver.get_window_state(safari_pid, wid) - assert after.has_text("type_text test"), ( - f"type_text result not in AX tree:\n{after.tree}" - ) + assert after.has_text("type_text test"), f"type_text result not in AX tree:\n{after.tree}" class TestSafariCheckbox: @@ -194,9 +197,7 @@ def test_checkbox_toggle(self, driver, safari_pid, ux_guard): time.sleep(0.4) after = driver.get_window_state(safari_pid, wid) - assert after.has_text("checked"), ( - f"Checkbox did not toggle to checked:\n{after.tree}" - ) + assert after.has_text("checked"), f"Checkbox did not toggle to checked:\n{after.tree}" class TestSafariSelect: @@ -224,16 +225,21 @@ def test_select_option_b(self, driver, safari_pid, ux_guard): else: # Fallback: set value directly. Safari AXPopUpButton uses the # display text ("Option B") not the option's value attr ("b"). - driver.call_tool("set_value", { - "pid": safari_pid, "window_id": wid, - "element_index": idx, "value": "Option B", - }) + driver.call_tool( + "set_value", + { + "pid": safari_pid, + "window_id": wid, + "element_index": idx, + "value": "Option B", + }, + ) time.sleep(0.6) after = driver.get_window_state(safari_pid, wid) - assert after.has_text("Option B") or after.has_text("selected: Option B"), ( - f"Dropdown did not select Option B:\n{after.tree}" - ) + assert after.has_text("Option B") or after.has_text( + "selected: Option B" + ), f"Dropdown did not select Option B:\n{after.tree}" class TestSafariTextarea: @@ -263,19 +269,22 @@ def test_textarea_input(self, driver, safari_pid, ux_guard): time.sleep(0.8) # longer settle for WKWebView textarea focus # Step 3: type — keystrokes go to the focused element - driver.call_tool("type_text_chars", { - "pid": safari_pid, - "text": "multi line", - "delay_ms": 50, - }) + driver.call_tool( + "type_text_chars", + { + "pid": safari_pid, + "text": "multi line", + "delay_ms": 50, + }, + ) time.sleep(0.6) after = driver.get_window_state(safari_pid, wid) val = Tree.ax_value(after.tree, ta_idx) print(f"\n textarea AX value: {val!r}") - assert after.has_text("multi line"), ( - f"Textarea text not in AX tree; val={val!r};\n{after.tree[:800]}" - ) + assert after.has_text( + "multi line" + ), f"Textarea text not in AX tree; val={val!r};\n{after.tree[:800]}" class TestSafariCanvas: @@ -296,6 +305,7 @@ def test_canvas_pixel_click_no_focus_steal(self, driver, safari_pid, ux_guard): print(f"\n screenshot: {before.screenshot_width}x{before.screenshot_height}") from harness.cv import decode, diff_ratio + before_img = decode(before.screenshot_b64) # Canvas is the last section on the page. With ~80px browser chrome diff --git a/libs/cua-driver/Tests/integration/test_tauri.py b/libs/cua-driver/Tests/integration/test_tauri.py index e1bb465f56..ed233bdc7c 100644 --- a/libs/cua-driver/Tests/integration/test_tauri.py +++ b/libs/cua-driver/Tests/integration/test_tauri.py @@ -11,23 +11,24 @@ from __future__ import annotations +import os import subprocess import sys import time -import os import pytest try: import requests as _requests + _REQUESTS_AVAILABLE = True except ImportError: _REQUESTS_AVAILABLE = False _HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, _HERE) -from harness.driver import Driver from harness import tree as Tree +from harness.driver import Driver _TAURI_API = "http://localhost:6769" @@ -55,8 +56,11 @@ def _http_reset(base_url: str = _TAURI_API) -> None: def _reactivate_focus(focus_monitor): _, pid = focus_monitor subprocess.run( - ["osascript", "-e", - f'tell application "System Events" to set frontmost of (first process whose unix id is {pid}) to true'], + [ + "osascript", + "-e", + f'tell application "System Events" to set frontmost of (first process whose unix id is {pid}) to true', + ], check=False, ) time.sleep(0.4) @@ -64,6 +68,7 @@ def _reactivate_focus(focus_monitor): # ── tests ───────────────────────────────────────────────────────────────────── + class TestTauriButton: def test_button_click(self, driver, tauri_app, ux_guard): _, pid, base_url = tauri_app @@ -128,9 +133,9 @@ def test_type_text(self, driver, tauri_app, ux_guard): key_events = [e for e in events if e.get("type") in ("key_down", "keydown", "input")] print(f" key events: {len(key_events)}") - assert text_in_tree or len(key_events) >= len("tauri test"), ( - f"Text not found in AX tree and insufficient key events:\n{after.tree}" - ) + assert text_in_tree or len(key_events) >= len( + "tauri test" + ), f"Text not found in AX tree and insufficient key events:\n{after.tree}" class TestTauriNoFocusSteal: diff --git a/libs/cua-driver/Tests/integration/test_webkit_js.py b/libs/cua-driver/Tests/integration/test_webkit_js.py index a3d46f697c..1ecb1c99bd 100644 --- a/libs/cua-driver/Tests/integration/test_webkit_js.py +++ b/libs/cua-driver/Tests/integration/test_webkit_js.py @@ -27,13 +27,14 @@ from driver_client import DriverClient, default_binary_path CONDUCTOR_BUNDLE = "com.conductor.app" -CONDUCTOR_PATH = "/Applications/Conductor.app" +CONDUCTOR_PATH = "/Applications/Conductor.app" # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- + def _tool_text(result: dict) -> str: for item in result.get("content", []): if item.get("type") == "text": @@ -48,13 +49,14 @@ def _conductor_installed() -> bool: def _kill_conductor() -> None: subprocess.run( ["osascript", "-e", 'tell application "Conductor" to quit'], - check=False, timeout=5, + check=False, + timeout=5, ) time.sleep(1) def _find_pid(text: str) -> int | None: - m = re.search(r'pid[=:\s]+(\d+)', text, re.IGNORECASE) + m = re.search(r"pid[=:\s]+(\d+)", text, re.IGNORECASE) return int(m.group(1)) if m else None @@ -63,14 +65,14 @@ def _main_window(client: DriverClient, pid: int) -> int | None: text = _tool_text(client.call_tool("list_windows", {"pid": pid})) for line in text.splitlines(): if "on_screen=True" in line or "is_on_screen=True" in line: - m = re.search(r'\[window_id:\s*(\d+)\]', line) + m = re.search(r"\[window_id:\s*(\d+)\]", line) if m: return int(m.group(1)) # Fall back to any titled window m = re.search(r'"[^"]+"\s+\[window_id:\s*(\d+)\]', text) if m: return int(m.group(1)) - m = re.search(r'\[window_id:\s*(\d+)\]', text) + m = re.search(r"\[window_id:\s*(\d+)\]", text) return int(m.group(1)) if m else None @@ -78,6 +80,7 @@ def _main_window(client: DriverClient, pid: int) -> int | None: # Conductor / Tauri AX fallback tests # --------------------------------------------------------------------------- + @unittest.skipUnless(_conductor_installed(), "Conductor not installed") class WebKitJSTests(unittest.TestCase): """AX-based page primitives work end-to-end against a live Conductor window.""" @@ -124,12 +127,15 @@ def tearDownClass(cls) -> None: def test_13_execute_javascript_returns_clear_error_for_wkwebview(self) -> None: """execute_javascript on a WKWebView app returns a clear error.""" with DriverClient(self.binary) as c: - result = c.call_tool("page", { - "pid": self._pid, - "window_id": self._window_id, - "action": "execute_javascript", - "javascript": "document.title", - }) + result = c.call_tool( + "page", + { + "pid": self._pid, + "window_id": self._window_id, + "action": "execute_javascript", + "javascript": "document.title", + }, + ) self.assertTrue(result.get("isError"), "Expected isError=True for WKWebView JS") text = _tool_text(result) self.assertIn("WKWebView", text) @@ -142,11 +148,14 @@ def test_13_execute_javascript_returns_clear_error_for_wkwebview(self) -> None: def test_11_ax_get_text_works_without_inspector(self) -> None: """get_text returns page text via AX tree (no inspector needed).""" with DriverClient(self.binary) as c: - result = c.call_tool("page", { - "pid": self._pid, - "window_id": self._window_id, - "action": "get_text", - }) + result = c.call_tool( + "page", + { + "pid": self._pid, + "window_id": self._window_id, + "action": "get_text", + }, + ) self.assertFalse(result.get("isError"), _tool_text(result)) text = _tool_text(result) self.assertTrue(len(text.strip()) > 0, "get_text returned empty content") @@ -158,19 +167,22 @@ def test_11_ax_get_text_works_without_inspector(self) -> None: def test_12_ax_query_dom_buttons(self) -> None: """query_dom for button elements returns AX tree results.""" with DriverClient(self.binary) as c: - result = c.call_tool("page", { - "pid": self._pid, - "window_id": self._window_id, - "action": "query_dom", - "css_selector": "button", - }) + result = c.call_tool( + "page", + { + "pid": self._pid, + "window_id": self._window_id, + "action": "query_dom", + "css_selector": "button", + }, + ) text = _tool_text(result) self.assertNotIn("Traceback", text) # Either found buttons (role key) or returned empty — both are valid if not result.get("isError"): self.assertTrue( "AXButton" in text or "role" in text or "[]" in text, - f"Unexpected query_dom output: {text[:200]}" + f"Unexpected query_dom output: {text[:200]}", ) # ----------------------------------------------------------------------- @@ -186,7 +198,7 @@ def test_chrome_not_detected_as_wkwebview(self) -> None: chrome_pid = None for line in text.splitlines(): if "com.google.Chrome" in line: - m = re.search(r'pid[=:\s]+(\d+)', line, re.IGNORECASE) + m = re.search(r"pid[=:\s]+(\d+)", line, re.IGNORECASE) if m: chrome_pid = int(m.group(1)) break @@ -197,22 +209,24 @@ def test_chrome_not_detected_as_wkwebview(self) -> None: with DriverClient(self.binary) as c: windows_text = _tool_text(c.call_tool("list_windows", {"pid": chrome_pid})) - m = re.search(r'\[window_id:\s*(\d+)\]', windows_text) + m = re.search(r"\[window_id:\s*(\d+)\]", windows_text) if not m: self.skipTest("No Chrome window found") window_id = int(m.group(1)) with DriverClient(self.binary) as c: - result = c.call_tool("page", { - "pid": chrome_pid, - "window_id": window_id, - "action": "execute_javascript", - "javascript": "1+1", - }) + result = c.call_tool( + "page", + { + "pid": chrome_pid, + "window_id": window_id, + "action": "execute_javascript", + "javascript": "1+1", + }, + ) text = _tool_text(result) self.assertNotIn( - "WKWebView", text, - f"Chrome was incorrectly detected as WKWebView app: {text[:200]}" + "WKWebView", text, f"Chrome was incorrectly detected as WKWebView app: {text[:200]}" ) diff --git a/libs/python/computer-server/computer_server/handlers/macos.py b/libs/python/computer-server/computer_server/handlers/macos.py index fdb97f5ab5..4fee050808 100644 --- a/libs/python/computer-server/computer_server/handlers/macos.py +++ b/libs/python/computer-server/computer_server/handlers/macos.py @@ -42,11 +42,11 @@ from ApplicationServices import kAXValueCGSizeType # type: ignore from ApplicationServices import kAXVisibleChildrenAttribute # type: ignore from ApplicationServices import kAXWindowsAttribute # type: ignore +from PIL import Image, ImageGrab from pynput.keyboard import Controller as KeyboardController from pynput.keyboard import Key from pynput.mouse import Button from pynput.mouse import Controller as MouseController -from PIL import Image, ImageGrab from Quartz.CoreGraphics import * # type: ignore from Quartz.CoreGraphics import CGPoint, CGSize # type: ignore @@ -58,6 +58,7 @@ logger = logging.getLogger(__name__) + # Fix: pynput's MouseController.position setter uses CGEventPost with # kCGEventMouseMoved internally, which silently fails in macOS VMs running # under Apple's Virtualization.framework (Tart, Lume, or any VZVirtualMachine- diff --git a/libs/python/computer-server/computer_server/main.py b/libs/python/computer-server/computer_server/main.py index daccf56d9c..78abbcad37 100644 --- a/libs/python/computer-server/computer_server/main.py +++ b/libs/python/computer-server/computer_server/main.py @@ -638,9 +638,7 @@ async def auth(self, container_name: str, api_key: str) -> bool: if _allow_unauthenticated_local(): logger.info("Explicit unauthenticated local development mode enabled") return True - logger.info( - "No CONTAINER_NAME set and CUA_ALLOW_UNAUTHENTICATED_LOCAL is not enabled" - ) + logger.info("No CONTAINER_NAME set and CUA_ALLOW_UNAUTHENTICATED_LOCAL is not enabled") return False # Layer 1: VM Identity Verification diff --git a/libs/python/computer-server/tests/test_auth_availability.py b/libs/python/computer-server/tests/test_auth_availability.py index 155938aad8..7595cb97f2 100644 --- a/libs/python/computer-server/tests/test_auth_availability.py +++ b/libs/python/computer-server/tests/test_auth_availability.py @@ -198,7 +198,9 @@ def test_explicit_local_dev_reaches_body_validation(self, clean_env, client): class TestCommandsEndpoint: - def test_static_command_metadata_available_before_handler_initialization(self, clean_env, client): + def test_static_command_metadata_available_before_handler_initialization( + self, clean_env, client + ): resp = client.get("/commands") assert resp.status_code == 200 @@ -217,9 +219,7 @@ def test_static_command_metadata_available_before_handler_initialization(self, c {"name": "append", "required": False, "default": False}, ] - def test_fresh_android_commands_metadata_lists_multitouch( - self, clean_env, client, monkeypatch - ): + def test_fresh_android_commands_metadata_lists_multitouch(self, clean_env, client, monkeypatch): monkeypatch.setattr(main, "OS_TYPE", "android") monkeypatch.setattr(main, "_command_handlers", None) diff --git a/libs/python/computer-server/tests/test_window_launch.py b/libs/python/computer-server/tests/test_window_launch.py index 5462a51a79..9062e8613a 100644 --- a/libs/python/computer-server/tests/test_window_launch.py +++ b/libs/python/computer-server/tests/test_window_launch.py @@ -1,7 +1,6 @@ from unittest.mock import Mock, patch import pytest - from computer_server.handlers.generic import GenericWindowHandler, build_launch_argv diff --git a/libs/python/cua-auto/tests/test_window_launch.py b/libs/python/cua-auto/tests/test_window_launch.py index e467c86c3b..6fb95b87a0 100644 --- a/libs/python/cua-auto/tests/test_window_launch.py +++ b/libs/python/cua-auto/tests/test_window_launch.py @@ -1,7 +1,6 @@ from unittest.mock import Mock, patch import pytest - from cua_auto.window import _build_launch_argv, launch diff --git a/libs/python/cua-sandbox/cua_sandbox/transport/cloud.py b/libs/python/cua-sandbox/cua_sandbox/transport/cloud.py index 8b518c49be..89e6d3ed28 100644 --- a/libs/python/cua-sandbox/cua_sandbox/transport/cloud.py +++ b/libs/python/cua-sandbox/cua_sandbox/transport/cloud.py @@ -388,10 +388,12 @@ async def _post_with_retry(self, path: str, body: dict) -> httpx.Response: return resp if attempt == _CREATE_MAX_RETRIES - 1: break - delay = _CREATE_RETRY_BASE_S * (2 ** attempt) + delay = _CREATE_RETRY_BASE_S * (2**attempt) logger.warning( "No sandbox capacity (503), retrying in %.1fs (attempt %d/%d)", - delay, attempt + 1, _CREATE_MAX_RETRIES, + delay, + attempt + 1, + _CREATE_MAX_RETRIES, ) await asyncio.sleep(delay) return resp # return last 503 response, caller will raise_for_status diff --git a/libs/python/cua-sandbox/cua_sandbox/transport/http.py b/libs/python/cua-sandbox/cua_sandbox/transport/http.py index 2a73ecc4dc..3d1ce479f9 100644 --- a/libs/python/cua-sandbox/cua_sandbox/transport/http.py +++ b/libs/python/cua-sandbox/cua_sandbox/transport/http.py @@ -98,10 +98,14 @@ async def _cmd(self, command: str, params: Optional[Dict[str, Any]] = None) -> D resp = await self._client.post("/cmd", json=body, timeout=req_timeout) if resp.status_code < 500 or attempt == _CMD_MAX_RETRIES - 1: break - backoff = _CMD_RETRY_BACKOFF_S * (2 ** attempt) + backoff = _CMD_RETRY_BACKOFF_S * (2**attempt) logger.debug( "[http] /cmd %s returned %d, retrying in %.1fs (attempt %d/%d)", - command, resp.status_code, backoff, attempt + 1, _CMD_MAX_RETRIES, + command, + resp.status_code, + backoff, + attempt + 1, + _CMD_MAX_RETRIES, ) await asyncio.sleep(backoff) diff --git a/libs/python/cua-sandbox/tests/test_transport_http_retry.py b/libs/python/cua-sandbox/tests/test_transport_http_retry.py index 39526111e7..7d9109d661 100644 --- a/libs/python/cua-sandbox/tests/test_transport_http_retry.py +++ b/libs/python/cua-sandbox/tests/test_transport_http_retry.py @@ -11,11 +11,9 @@ from __future__ import annotations import json -from itertools import cycle import httpx import pytest - from cua_sandbox.transport.http import HTTPTransport pytestmark = pytest.mark.asyncio @@ -61,11 +59,13 @@ async def _no_sleep(_seconds): monkeypatch.setattr(http_mod.asyncio, "sleep", _no_sleep) - responses = iter([ - httpx.Response(503, text="service unavailable"), - httpx.Response(502, text="bad gateway"), - httpx.Response(200, text=_sse_body({"ok": True})), - ]) + responses = iter( + [ + httpx.Response(503, text="service unavailable"), + httpx.Response(502, text="bad gateway"), + httpx.Response(200, text=_sse_body({"ok": True})), + ] + ) calls = [] def handler(request: httpx.Request) -> httpx.Response: From 8ced833efdb5befe3a3055f0a00706beb8378584 Mon Sep 17 00:00:00 2001 From: zeroxjf Date: Mon, 18 May 2026 12:26:33 -0400 Subject: [PATCH 3/5] Address security review dependency floors --- changelog/2026-03-14.md | 3 + libs/cua-bench/pyproject.toml | 2 +- libs/cua-bench/uv.lock | 4 +- libs/python/agent/pyproject.toml | 2 +- libs/python/computer-server/README.md | 6 ++ .../computer-server/computer_server/cli.py | 4 +- .../computer_server/handlers/generic.py | 10 ++- libs/python/computer-server/pyproject.toml | 2 +- .../tests/test_window_launch.py | 14 ++++ libs/python/cua-auto/cua_auto/window.py | 10 ++- .../cua-auto/tests/test_window_launch.py | 14 ++++ libs/python/cua-cli/pyproject.toml | 4 +- libs/python/cua-cli/uv.lock | 74 ++++++++++++++----- libs/python/cua-sandbox/uv.lock | 2 +- pyproject.toml | 2 +- uv.lock | 57 ++++++++++---- 16 files changed, 166 insertions(+), 44 deletions(-) diff --git a/changelog/2026-03-14.md b/changelog/2026-03-14.md index f7090bb44c..9d7104c41d 100644 --- a/changelog/2026-03-14.md +++ b/changelog/2026-03-14.md @@ -1,5 +1,8 @@ ### Highlights +- **Computer server bind default** — `computer_server` now binds to `127.0.0.1` + by default. Use `--host 0.0.0.0` when the server needs to accept external + connections. - **N1 browser-use agent loop** — Added the Yutori N1 browser-use agent loop as a native loop, with adapter and loop registration integrated into the agent stack. - **GPT-5.4 native agent loop support** — Added GPT-5.4 support for the Computer tool, enabling native agent loop execution. - **Interactive & non-interactive shell on sandboxes** — New `cua sb shell` and `cua sb exec` commands for interactive and non-interactive sandbox access. diff --git a/libs/cua-bench/pyproject.toml b/libs/cua-bench/pyproject.toml index eed0e3dd93..46f01aad39 100644 --- a/libs/cua-bench/pyproject.toml +++ b/libs/cua-bench/pyproject.toml @@ -71,7 +71,7 @@ browser = [ # Agent support agents = [ "cua-agent>=0.8.0", - "litellm>=1.83.7,<1.84", + "litellm>=1.83.10,<1.84", "anthropic>=0.26.0", ] diff --git a/libs/cua-bench/uv.lock b/libs/cua-bench/uv.lock index e7f435b414..830a1c53d8 100644 --- a/libs/cua-bench/uv.lock +++ b/libs/cua-bench/uv.lock @@ -512,7 +512,7 @@ requires-dist = [ { name = "gradio", marker = "extra == 'ui'", specifier = ">=6.0.0" }, { name = "httpx", specifier = ">=0.27.0" }, { name = "hud-python", marker = "extra == 'hud'", specifier = "==0.4.52" }, - { name = "litellm", specifier = ">=1.83.7,<1.84" }, + { name = "litellm", specifier = ">=1.83.10,<1.84" }, { name = "mlx-vlm", marker = "sys_platform == 'darwin' and extra == 'all'", specifier = ">=0.1.27" }, { name = "mlx-vlm", marker = "sys_platform == 'darwin' and extra == 'uitars-mlx'", specifier = ">=0.1.27" }, { name = "pillow", specifier = ">=10.0.0" }, @@ -662,7 +662,7 @@ requires-dist = [ { name = "html5lib", specifier = ">=1.1" }, { name = "icoextract", marker = "extra == 'windows'", specifier = ">=0.2.0" }, { name = "jinja2", specifier = ">=3.1.0" }, - { name = "litellm", marker = "extra == 'agents'", specifier = ">=1.83.7,<1.84" }, + { name = "litellm", marker = "extra == 'agents'", specifier = ">=1.83.10,<1.84" }, { name = "matplotlib", specifier = ">=3.8.0" }, { name = "numpy", specifier = ">=2.0.0" }, { name = "opentelemetry-api", marker = "extra == 'otel'", specifier = ">=1.20.0" }, diff --git a/libs/python/agent/pyproject.toml b/libs/python/agent/pyproject.toml index 1b3c29a302..4cdc7ceeec 100644 --- a/libs/python/agent/pyproject.toml +++ b/libs/python/agent/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "python-dotenv>=1.0.1", "cua-core>=0.3.0,<0.4.0", "certifi>=2024.2.2", - "litellm>=1.83.7,<1.84", + "litellm>=1.83.10,<1.84", "Pillow>=10.0.0" ] requires-python = ">=3.11,<3.14" diff --git a/libs/python/computer-server/README.md b/libs/python/computer-server/README.md index 243911e573..ac9a150554 100644 --- a/libs/python/computer-server/README.md +++ b/libs/python/computer-server/README.md @@ -32,10 +32,16 @@ python -m computer_server # Or with custom port python -m computer_server --port 8080 +# Allow external access explicitly +python -m computer_server --host 0.0.0.0 + # With resolution scaling (useful for Retina displays or VMs) python -m computer_server --width 1512 --height 982 ``` +By default the server binds to `127.0.0.1`. Deployments that need access from +other hosts should pass `--host 0.0.0.0` or another explicit interface. + This provides: - HTTP API at `/ws`, `/cmd`, `/status` endpoints diff --git a/libs/python/computer-server/computer_server/cli.py b/libs/python/computer-server/computer_server/cli.py index 8d82730c85..891b0fc696 100644 --- a/libs/python/computer-server/computer_server/cli.py +++ b/libs/python/computer-server/computer_server/cli.py @@ -30,7 +30,9 @@ def parse_args(args: Optional[List[str]] = None) -> argparse.Namespace: help="Auto-detect and log the actual screen resolution at startup", ) parser.add_argument( - "--host", default="127.0.0.1", help="Host to bind the server to (default: 127.0.0.1)" + "--host", + default="127.0.0.1", + help="Host to bind the server to (default: 127.0.0.1; use 0.0.0.0 for external access)", ) parser.add_argument( "--port", type=int, default=8000, help="Port to bind the server to (default: 8000)" diff --git a/libs/python/computer-server/computer_server/handlers/generic.py b/libs/python/computer-server/computer_server/handlers/generic.py index 1b00bed901..ae5aad5c26 100644 --- a/libs/python/computer-server/computer_server/handlers/generic.py +++ b/libs/python/computer-server/computer_server/handlers/generic.py @@ -28,12 +28,20 @@ def build_launch_argv(app: str, args: Optional[list[str]] = None) -> list[str]: if args is not None: return [app, *args] - argv = shlex.split(app) + argv = shlex.split(app, posix=(os.name != "nt")) + if os.name == "nt": + argv = [_strip_wrapping_quotes(arg) for arg in argv] if not argv: raise ValueError("app must not be empty") return argv +def _strip_wrapping_quotes(value: str) -> str: + if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}: + return value[1:-1] + return value + + def resolve_path(path: str) -> Path: """Resolve a path to its absolute path. Expand ~ to the user's home directory. diff --git a/libs/python/computer-server/pyproject.toml b/libs/python/computer-server/pyproject.toml index 56d14cf12a..490e0fdd86 100644 --- a/libs/python/computer-server/pyproject.toml +++ b/libs/python/computer-server/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "websockets>=12.0", "pywinctl>=0.4.1", "playwright>=1.40.0", - "fastmcp>=3.2.0,<4", + "fastmcp>=3.3.0,<4", # PTY support (wraps cua-auto terminal engine) "cua-auto>=0.1.0", # OS-specific runtime deps diff --git a/libs/python/computer-server/tests/test_window_launch.py b/libs/python/computer-server/tests/test_window_launch.py index 9062e8613a..ac3cddf8d3 100644 --- a/libs/python/computer-server/tests/test_window_launch.py +++ b/libs/python/computer-server/tests/test_window_launch.py @@ -1,6 +1,7 @@ from unittest.mock import Mock, patch import pytest +from computer_server.handlers import generic from computer_server.handlers.generic import GenericWindowHandler, build_launch_argv @@ -24,6 +25,19 @@ def test_build_launch_argv_uses_explicit_args_literally(): ] +def test_build_launch_argv_preserves_windows_paths(monkeypatch): + monkeypatch.setattr(generic.os, "name", "nt") + + assert build_launch_argv('"C:\\Program Files\\App\\app.exe" --flag') == [ + "C:\\Program Files\\App\\app.exe", + "--flag", + ] + assert build_launch_argv("C:\\Tools\\app.exe --flag") == [ + "C:\\Tools\\app.exe", + "--flag", + ] + + @pytest.mark.asyncio async def test_launch_uses_argv_without_shell(): proc = Mock(pid=1234) diff --git a/libs/python/cua-auto/cua_auto/window.py b/libs/python/cua-auto/cua_auto/window.py index 9eaa50d746..d55d2ca553 100644 --- a/libs/python/cua-auto/cua_auto/window.py +++ b/libs/python/cua-auto/cua_auto/window.py @@ -34,12 +34,20 @@ def _get_by_handle(handle: Any) -> Optional[Any]: def _build_launch_argv(app: str, args: Optional[List[str]] = None) -> List[str]: if args is not None: return [app, *args] - argv = shlex.split(app) + argv = shlex.split(app, posix=(os.name != "nt")) + if os.name == "nt": + argv = [_strip_wrapping_quotes(arg) for arg in argv] if not argv: raise ValueError("app must not be empty") return argv +def _strip_wrapping_quotes(value: str) -> str: + if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}: + return value[1:-1] + return value + + # ── Active window ───────────────────────────────────────────────────────────── diff --git a/libs/python/cua-auto/tests/test_window_launch.py b/libs/python/cua-auto/tests/test_window_launch.py index 6fb95b87a0..1766b24de6 100644 --- a/libs/python/cua-auto/tests/test_window_launch.py +++ b/libs/python/cua-auto/tests/test_window_launch.py @@ -1,5 +1,6 @@ from unittest.mock import Mock, patch +import cua_auto.window as window import pytest from cua_auto.window import _build_launch_argv, launch @@ -24,6 +25,19 @@ def test_build_launch_argv_uses_explicit_args_literally(): ] +def test_build_launch_argv_preserves_windows_paths(monkeypatch): + monkeypatch.setattr(window.os, "name", "nt") + + assert _build_launch_argv('"C:\\Program Files\\App\\app.exe" --flag') == [ + "C:\\Program Files\\App\\app.exe", + "--flag", + ] + assert _build_launch_argv("C:\\Tools\\app.exe --flag") == [ + "C:\\Tools\\app.exe", + "--flag", + ] + + def test_launch_uses_argv_without_shell(): proc = Mock(pid=1234) with patch("cua_auto.window.subprocess.Popen", return_value=proc) as popen: diff --git a/libs/python/cua-cli/pyproject.toml b/libs/python/cua-cli/pyproject.toml index 1a2e3ab6f1..aab6591e6d 100644 --- a/libs/python/cua-cli/pyproject.toml +++ b/libs/python/cua-cli/pyproject.toml @@ -45,11 +45,11 @@ dependencies = [ [project.optional-dependencies] # MCP server support mcp = [ - "fastmcp>=3.2.0", + "fastmcp>=3.3.0", ] # Skills recording with LLM captioning skills = [ - "litellm>=1.83.7,<1.84", + "litellm>=1.83.10,<1.84", ] # Full installation all = [ diff --git a/libs/python/cua-cli/uv.lock b/libs/python/cua-cli/uv.lock index da8755eb08..1d9915a741 100644 --- a/libs/python/cua-cli/uv.lock +++ b/libs/python/cua-cli/uv.lock @@ -129,14 +129,15 @@ wheels = [ [[package]] name = "authlib" -version = "1.6.9" +version = "1.7.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, + { name = "joserfc" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/af/98/00d3dd826d46959ad8e32af2dbb2398868fd9fd0683c26e56d0789bd0e68/authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04", size = 165134, upload-time = "2026-03-02T07:44:01.998Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/98/7d93f30d029643c0275dbc0bd6d5a6f670661ee6c9a94d93af7ab4887600/authlib-1.7.2.tar.gz", hash = "sha256:2cea25fefcd4e7173bdf1372c0afc265c8034b23a8cd5dcb6a9164b826c64231", size = 176511, upload-time = "2026-05-06T08:10:23.116Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/23/b65f568ed0c22f1efacb744d2db1a33c8068f384b8c9b482b52ebdbc3ef6/authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3", size = 244197, upload-time = "2026-03-02T07:44:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/fb/95/adcb68e20c34162e9135f370d6e31737719c2b6f94bc953fe7ed1f10fe21/authlib-1.7.2-py2.py3-none-any.whl", hash = "sha256:3e1faedc9d87e7d56a164eca3ccb6ace0d61b94abe83e92242f8dc8bba9b4a9f", size = 259548, upload-time = "2026-05-06T08:10:21.436Z" }, ] [[package]] @@ -444,8 +445,8 @@ requires-dist = [ { name = "cua-cli", extras = ["mcp", "skills"], marker = "extra == 'all'" }, { name = "cua-computer", specifier = ">=0.5.0" }, { name = "cua-core", specifier = ">=0.3.0,<0.4.0" }, - { name = "fastmcp", marker = "extra == 'mcp'", specifier = ">=3.2.0" }, - { name = "litellm", marker = "extra == 'skills'", specifier = ">=1.83.7,<1.84" }, + { name = "fastmcp", marker = "extra == 'mcp'", specifier = ">=3.3.0" }, + { name = "litellm", marker = "extra == 'skills'", specifier = ">=1.83.10,<1.84" }, { name = "pillow", specifier = ">=10.0.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.0" }, @@ -583,9 +584,43 @@ wheels = [ [[package]] name = "fastmcp" -version = "3.2.4" +version = "3.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "fastmcp-slim", extra = ["client", "server"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/a9/5c5a01b6abd5346bf60b97cfd29e4a86661940c27dd562bfcda07fd03519/fastmcp-3.3.1.tar.gz", hash = "sha256:979362ea557de42a5f40342563c7e4b236bcc8e7cd192715f50030695d1a71cd", size = 28681699, upload-time = "2026-05-15T15:50:39.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/11/6b1bdada6ccfe647d615ae63f9106f8136aec17971e9361546af01c7d38e/fastmcp-3.3.1-py3-none-any.whl", hash = "sha256:862440c5c4d281363a5995eee59d77f0f7cac1f18869038729cecf03b02fc522", size = 7903, upload-time = "2026-05-15T15:50:36.424Z" }, +] + +[[package]] +name = "fastmcp-slim" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "pydantic", extra = ["email"] }, + { name = "pydantic-settings" }, + { name = "python-dotenv" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/a0/627103e517e1d0d6f1eec633d5662d13e776f01b45ad188e4f5f7478b438/fastmcp_slim-3.3.1.tar.gz", hash = "sha256:0957835fc59452e143ab2f4b7836d2d2df9b2d9958408edc79ba8b56232b2a88", size = 567007, upload-time = "2026-05-15T15:50:10.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/ee/97047f4cc2d7b1d46670d08d8ad01a96e7a748cc01c0b4b351ad8eddbc7a/fastmcp_slim-3.3.1-py3-none-any.whl", hash = "sha256:6cf1c2d77e3adb0d409d6825ed6b0b2a999062973e00b8eea03bd48bf9b4c043", size = 738644, upload-time = "2026-05-15T15:50:08.336Z" }, +] + +[package.optional-dependencies] +client = [ + { name = "authlib" }, + { name = "exceptiongroup" }, + { name = "httpx" }, + { name = "mcp" }, + { name = "opentelemetry-api" }, + { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] }, +] +server = [ { name = "authlib" }, { name = "cyclopts" }, { name = "exceptiongroup" }, @@ -597,22 +632,15 @@ dependencies = [ { name = "openapi-pydantic" }, { name = "opentelemetry-api" }, { name = "packaging" }, - { name = "platformdirs" }, { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] }, - { name = "pydantic", extra = ["email"] }, { name = "pyperclip" }, - { name = "python-dotenv" }, + { name = "python-multipart" }, { name = "pyyaml" }, - { name = "rich" }, { name = "uncalled-for" }, { name = "uvicorn" }, { name = "watchfiles" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9c/13/29544fbc6dfe45ea38046af0067311e0bad7acc7d1f2ad38bb08f2409fe2/fastmcp-3.2.4.tar.gz", hash = "sha256:083ecb75b44a4169e7fc0f632f94b781bdb0ff877c6b35b9877cbb566fd4d4d1", size = 28746127, upload-time = "2026-04-14T01:42:24.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/76/b310d52fa0e30d39bd937eb58ec2c1f1ea1b5f519f0575e9dd9612f01deb/fastmcp-3.2.4-py3-none-any.whl", hash = "sha256:e6c9c429171041455e47ab94bb3f83c4657622a0ec28922f6940053959bd58a9", size = 728599, upload-time = "2026-04-14T01:42:26.85Z" }, -] [[package]] name = "fastuuid" @@ -945,6 +973,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, ] +[[package]] +name = "joserfc" +version = "1.6.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/dc/5f768c2e391e9afabe5d18e3221346deb5fb6338565f1ccc9e7c6d7befdd/joserfc-1.6.5.tar.gz", hash = "sha256:1482a7db78fb4602e44ed89e51b599d052e091288c7c532c5b694e20149dec48", size = 231881, upload-time = "2026-05-06T04:58:13.408Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/3b/ad1cb22e75c963b1f07c8a2329bf47227ce7e4361df5eb2fb101b2ce33ef/joserfc-1.6.5-py3-none-any.whl", hash = "sha256:e9878a0f8243fe7b95e11fdda81374ca9f7a689e302751579d3dfdeec559675e", size = 70464, upload-time = "2026-05-06T04:58:11.668Z" }, +] + [[package]] name = "jsonref" version = "1.1.0" @@ -4149,11 +4189,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.22" +version = "0.0.29" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/fe/70bd71a6738b09a0bdf6480ca6436b167469ca4578b2a0efbe390b4b0e70/python_multipart-0.0.29.tar.gz", hash = "sha256:643e93849196645e2dbdd81a0f8829a23123ad7f797a84a364c6fb3563f18904", size = 45678, upload-time = "2026-05-17T17:29:47.654Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, + { url = "https://files.pythonhosted.org/packages/8f/cb/769cfc37177252872a45a71f3fbdde9d51b471a3f3c14bfe95dde3407386/python_multipart-0.0.29-py3-none-any.whl", hash = "sha256:2ddcc971cef266225f54f552d8fa10bcfbb1f14446caec199060daac59ff2d69", size = 29640, upload-time = "2026-05-17T17:29:45.69Z" }, ] [[package]] diff --git a/libs/python/cua-sandbox/uv.lock b/libs/python/cua-sandbox/uv.lock index 108f0bc1d0..c68a528c14 100644 --- a/libs/python/cua-sandbox/uv.lock +++ b/libs/python/cua-sandbox/uv.lock @@ -450,7 +450,7 @@ requires-dist = [ { name = "gradio", marker = "extra == 'ui'", specifier = ">=6.0.0" }, { name = "httpx", specifier = ">=0.27.0" }, { name = "hud-python", marker = "extra == 'hud'", specifier = "==0.4.52" }, - { name = "litellm", specifier = ">=1.83.7,<1.84" }, + { name = "litellm", specifier = ">=1.83.10,<1.84" }, { name = "mlx-vlm", marker = "sys_platform == 'darwin' and extra == 'all'", specifier = ">=0.1.27" }, { name = "mlx-vlm", marker = "sys_platform == 'darwin' and extra == 'uitars-mlx'", specifier = ">=0.1.27" }, { name = "pillow", specifier = ">=10.0.0" }, diff --git a/pyproject.toml b/pyproject.toml index a0b5ce4d2b..ff22b358c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ docs-scripts = [ "lancedb>=0.4.0", "sentence-transformers>=2.2.0", "pyarrow>=14.0.1", - "fastmcp>=3.2.0", + "fastmcp>=3.3.0", "pydantic>=2.0.0", "pandas>=2.0.0", "markdown-it-py>=3.0.0", diff --git a/uv.lock b/uv.lock index 421653b25f..e3f2bb72bf 100644 --- a/uv.lock +++ b/uv.lock @@ -1203,7 +1203,7 @@ requires-dist = [ { name = "gradio", marker = "extra == 'ui'", specifier = ">=6.0.0" }, { name = "httpx", specifier = ">=0.27.0" }, { name = "hud-python", marker = "extra == 'hud'", specifier = "==0.4.52" }, - { name = "litellm", specifier = ">=1.83.7,<1.84" }, + { name = "litellm", specifier = ">=1.83.10,<1.84" }, { name = "mlx-vlm", marker = "sys_platform == 'darwin' and extra == 'all'", specifier = ">=0.1.27" }, { name = "mlx-vlm", marker = "sys_platform == 'darwin' and extra == 'uitars-mlx'", specifier = ">=0.1.27" }, { name = "pillow", specifier = ">=10.0.0" }, @@ -1382,7 +1382,7 @@ requires-dist = [ { name = "cua-auto", editable = "libs/python/cua-auto" }, { name = "cua-core", editable = "libs/python/core" }, { name = "fastapi", specifier = ">=0.111.0" }, - { name = "fastmcp", specifier = ">=3.2.0,<4" }, + { name = "fastmcp", specifier = ">=3.3.0,<4" }, { name = "grpcio", specifier = "==1.78.0" }, { name = "pillow", specifier = ">=10.2.0" }, { name = "playwright", specifier = ">=1.40.0" }, @@ -1589,7 +1589,7 @@ docs = [ ] docs-scripts = [ { name = "crawl4ai", specifier = ">=0.8.0" }, - { name = "fastmcp", specifier = ">=3.2.0" }, + { name = "fastmcp", specifier = ">=3.3.0" }, { name = "gitpython", specifier = ">=3.1.43" }, { name = "griffe", specifier = ">=2.0.0" }, { name = "lancedb", specifier = ">=0.4.0" }, @@ -1924,9 +1924,43 @@ wheels = [ [[package]] name = "fastmcp" -version = "3.2.4" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastmcp-slim", extra = ["client", "server"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/a9/5c5a01b6abd5346bf60b97cfd29e4a86661940c27dd562bfcda07fd03519/fastmcp-3.3.1.tar.gz", hash = "sha256:979362ea557de42a5f40342563c7e4b236bcc8e7cd192715f50030695d1a71cd", size = 28681699, upload-time = "2026-05-15T15:50:39.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/11/6b1bdada6ccfe647d615ae63f9106f8136aec17971e9361546af01c7d38e/fastmcp-3.3.1-py3-none-any.whl", hash = "sha256:862440c5c4d281363a5995eee59d77f0f7cac1f18869038729cecf03b02fc522", size = 7903, upload-time = "2026-05-15T15:50:36.424Z" }, +] + +[[package]] +name = "fastmcp-slim" +version = "3.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "platformdirs" }, + { name = "pydantic", extra = ["email"] }, + { name = "pydantic-settings" }, + { name = "python-dotenv" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/a0/627103e517e1d0d6f1eec633d5662d13e776f01b45ad188e4f5f7478b438/fastmcp_slim-3.3.1.tar.gz", hash = "sha256:0957835fc59452e143ab2f4b7836d2d2df9b2d9958408edc79ba8b56232b2a88", size = 567007, upload-time = "2026-05-15T15:50:10.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/ee/97047f4cc2d7b1d46670d08d8ad01a96e7a748cc01c0b4b351ad8eddbc7a/fastmcp_slim-3.3.1-py3-none-any.whl", hash = "sha256:6cf1c2d77e3adb0d409d6825ed6b0b2a999062973e00b8eea03bd48bf9b4c043", size = 738644, upload-time = "2026-05-15T15:50:08.336Z" }, +] + +[package.optional-dependencies] +client = [ + { name = "authlib" }, + { name = "exceptiongroup" }, + { name = "httpx" }, + { name = "mcp" }, + { name = "opentelemetry-api" }, + { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] }, +] +server = [ { name = "authlib" }, { name = "cyclopts" }, { name = "exceptiongroup" }, @@ -1938,22 +1972,15 @@ dependencies = [ { name = "openapi-pydantic" }, { name = "opentelemetry-api" }, { name = "packaging" }, - { name = "platformdirs" }, { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] }, - { name = "pydantic", extra = ["email"] }, { name = "pyperclip" }, - { name = "python-dotenv" }, + { name = "python-multipart" }, { name = "pyyaml" }, - { name = "rich" }, { name = "uncalled-for" }, { name = "uvicorn" }, { name = "watchfiles" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9c/13/29544fbc6dfe45ea38046af0067311e0bad7acc7d1f2ad38bb08f2409fe2/fastmcp-3.2.4.tar.gz", hash = "sha256:083ecb75b44a4169e7fc0f632f94b781bdb0ff877c6b35b9877cbb566fd4d4d1", size = 28746127, upload-time = "2026-04-14T01:42:24.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/76/b310d52fa0e30d39bd937eb58ec2c1f1ea1b5f519f0575e9dd9612f01deb/fastmcp-3.2.4-py3-none-any.whl", hash = "sha256:e6c9c429171041455e47ab94bb3f83c4657622a0ec28922f6940053959bd58a9", size = 728599, upload-time = "2026-04-14T01:42:26.85Z" }, -] [[package]] name = "fastuuid" @@ -8522,11 +8549,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.21" +version = "0.0.29" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196, upload-time = "2025-12-17T09:24:22.446Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/fe/70bd71a6738b09a0bdf6480ca6436b167469ca4578b2a0efbe390b4b0e70/python_multipart-0.0.29.tar.gz", hash = "sha256:643e93849196645e2dbdd81a0f8829a23123ad7f797a84a364c6fb3563f18904", size = 45678, upload-time = "2026-05-17T17:29:47.654Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/8f/cb/769cfc37177252872a45a71f3fbdde9d51b471a3f3c14bfe95dde3407386/python_multipart-0.0.29-py3-none-any.whl", hash = "sha256:2ddcc971cef266225f54f552d8fa10bcfbb1f14446caec199060daac59ff2d69", size = 29640, upload-time = "2026-05-17T17:29:45.69Z" }, ] [[package]] From d12e782bd618af501775725867cdb3e552edec83 Mon Sep 17 00:00:00 2001 From: zeroxjf Date: Mon, 18 May 2026 12:30:51 -0400 Subject: [PATCH 4/5] Move host bind note to current changelog --- changelog/2026-03-14.md | 3 --- changelog/2026-05-18.md | 5 +++++ 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 changelog/2026-05-18.md diff --git a/changelog/2026-03-14.md b/changelog/2026-03-14.md index 9d7104c41d..f7090bb44c 100644 --- a/changelog/2026-03-14.md +++ b/changelog/2026-03-14.md @@ -1,8 +1,5 @@ ### Highlights -- **Computer server bind default** — `computer_server` now binds to `127.0.0.1` - by default. Use `--host 0.0.0.0` when the server needs to accept external - connections. - **N1 browser-use agent loop** — Added the Yutori N1 browser-use agent loop as a native loop, with adapter and loop registration integrated into the agent stack. - **GPT-5.4 native agent loop support** — Added GPT-5.4 support for the Computer tool, enabling native agent loop execution. - **Interactive & non-interactive shell on sandboxes** — New `cua sb shell` and `cua sb exec` commands for interactive and non-interactive sandbox access. diff --git a/changelog/2026-05-18.md b/changelog/2026-05-18.md new file mode 100644 index 0000000000..6ad1980283 --- /dev/null +++ b/changelog/2026-05-18.md @@ -0,0 +1,5 @@ +### Highlights + +- **Computer server bind default** — `computer_server` now binds to `127.0.0.1` + by default. Use `--host 0.0.0.0` when the server needs to accept external + connections. From 58ab01da0ab7a957f8c6c9fcadff8d0aa5171a8a Mon Sep 17 00:00:00 2001 From: zeroxjf Date: Mon, 18 May 2026 12:39:38 -0400 Subject: [PATCH 5/5] Clarify server host default --- libs/python/computer-server/computer_server/server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/python/computer-server/computer_server/server.py b/libs/python/computer-server/computer_server/server.py index f9de93b1a0..db52a0ba2c 100644 --- a/libs/python/computer-server/computer_server/server.py +++ b/libs/python/computer-server/computer_server/server.py @@ -45,7 +45,8 @@ def __init__( Initialize the server. Args: - host: Host to bind the server to + host: Host to bind the server to. Defaults to localhost; pass + "0.0.0.0" explicitly for external access. port: Port to bind the server to log_level: Logging level (debug, info, warning, error, critical) ssl_keyfile: Path to SSL private key file (for HTTPS)