From 56368e048a13942868d8af2f528781c0eefbb7b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:37:10 +0000 Subject: [PATCH 01/13] chore(deps): bump tornado from 6.5.7 to 6.5.8 in /envs/wildfire_env Bumps [tornado](https://github.com/tornadoweb/tornado) from 6.5.7 to 6.5.8. - [Changelog](https://github.com/tornadoweb/tornado/blob/master/docs/releases.rst) - [Commits](https://github.com/tornadoweb/tornado/compare/v6.5.7...v6.5.8) --- updated-dependencies: - dependency-name: tornado dependency-version: 6.5.8 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- envs/wildfire_env/uv.lock | 98 +++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/envs/wildfire_env/uv.lock b/envs/wildfire_env/uv.lock index c64a97569b..bcf9a57035 100644 --- a/envs/wildfire_env/uv.lock +++ b/envs/wildfire_env/uv.lock @@ -502,7 +502,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } wheels = [ @@ -580,7 +580,7 @@ resolution-markers = [ "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } wheels = [ @@ -1385,17 +1385,17 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version < '3.11'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "jedi", marker = "python_full_version < '3.11'" }, - { name = "matplotlib-inline", marker = "python_full_version < '3.11'" }, - { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version < '3.11'" }, - { name = "pygments", marker = "python_full_version < '3.11'" }, - { name = "stack-data", marker = "python_full_version < '3.11'" }, - { name = "traitlets", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "exceptiongroup" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/40/18/f8598d287006885e7136451fdea0755af4ebcbfe342836f24deefaed1164/ipython-8.39.0.tar.gz", hash = "sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624", size = 5513971, upload-time = "2026-03-27T10:02:13.94Z" } wheels = [ @@ -1418,18 +1418,18 @@ resolution-markers = [ "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version >= '3.11'" }, - { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" }, - { name = "jedi", marker = "python_full_version >= '3.11'" }, - { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, - { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, - { name = "psutil", marker = "python_full_version >= '3.11'" }, - { name = "pygments", marker = "python_full_version >= '3.11'" }, - { name = "stack-data", marker = "python_full_version >= '3.11'" }, - { name = "traitlets", marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "psutil" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/87cda5842cf5c31837c06ddb588e11c3c35d8ece89b7a0108c06b8c9b00a/ipython-9.13.0.tar.gz", hash = "sha256:7e834b6afc99f020e3f05966ced34792f40267d64cb1ea9043886dab0dde5967", size = 4430549, upload-time = "2026-04-24T12:24:55.221Z" } wheels = [ @@ -1441,7 +1441,7 @@ name = "ipython-pygments-lexers" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } wheels = [ @@ -2449,10 +2449,10 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "python-dateutil", marker = "python_full_version < '3.11'" }, - { name = "pytz", marker = "python_full_version < '3.11'" }, - { name = "tzdata", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ @@ -2521,9 +2521,9 @@ resolution-markers = [ "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, - { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" } }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" } wheels = [ @@ -2599,7 +2599,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "ptyprocess" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -3446,8 +3446,8 @@ name = "secretstorage" version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, - { name = "jeepney", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "cryptography" }, + { name = "jeepney" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } wheels = [ @@ -3604,19 +3604,19 @@ wheels = [ [[package]] name = "tornado" -version = "6.5.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/64/24/95ec527ad67b76d59299e5465b3935d05e4294b7e0290a3924b7487df30b/tornado-6.5.7.tar.gz", hash = "sha256:66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2", size = 519252, upload-time = "2026-06-08T17:34:51.232Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163", size = 448543, upload-time = "2026-06-08T17:34:38.052Z" }, - { url = "https://files.pythonhosted.org/packages/92/4f/090b1431e5a43df696feceffc268c5383cc079ecb5f08ce58f917109aafe/tornado-6.5.7-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9da38de27f1da3b78a966f0dae12b5a1ea9afe72ca805d84ff06508272ddf100", size = 446707, upload-time = "2026-06-08T17:34:39.594Z" }, - { url = "https://files.pythonhosted.org/packages/37/d8/ef374952fd5da67d4463122c2b8e5a96536ec10b4b339254c6dcde81d01c/tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972", size = 449774, upload-time = "2026-06-08T17:34:41.204Z" }, - { url = "https://files.pythonhosted.org/packages/35/37/d434c73f4c6e014b745b9b37085f34f40c022f007efff3d7fe65991899f3/tornado-6.5.7-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a46347a18f23fb92b396beebe0fb78f61dda0cc302445202c16203d8a18848b", size = 450745, upload-time = "2026-06-08T17:34:42.531Z" }, - { url = "https://files.pythonhosted.org/packages/b6/2b/56b9aff361d7f1ab728a805ec7d7ea835f8807afa9f5cc690ea0e630efb9/tornado-6.5.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7778b30bef919231265e91c69963ce0f49a1e9c07ac900bbe75b19ce2575ba92", size = 450578, upload-time = "2026-06-08T17:34:43.787Z" }, - { url = "https://files.pythonhosted.org/packages/02/30/a7444fb23aa76860a14198fab96ac79f1866b0a6e19e26c4381b0938e50f/tornado-6.5.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e726f0c75da7726eec023aa62751ff8878bd2737e34fbdd33b1ae5897d2200f5", size = 449985, upload-time = "2026-06-08T17:34:45.326Z" }, - { url = "https://files.pythonhosted.org/packages/5c/42/5f0e56c01e8d9d36f4e23f367b85ae6cae0c1ecddd5e6977d8388ad27488/tornado-6.5.7-cp39-abi3-win32.whl", hash = "sha256:f8de3bf12d3efdd0cbe7c8887868198f8a91415e3f29fcf258d9b8eb7b1d9ae4", size = 451047, upload-time = "2026-06-08T17:34:46.784Z" }, - { url = "https://files.pythonhosted.org/packages/c9/a4/b393076ffb21b469eec5b328a0534cf03a3b90bfc6b1f09507cdd075d938/tornado-6.5.7-cp39-abi3-win_amd64.whl", hash = "sha256:de942f843533a039ef9fa3d9c88c7cd8a7c94553fb5ad0154270989b3d99a2c4", size = 451485, upload-time = "2026-06-08T17:34:48.248Z" }, - { url = "https://files.pythonhosted.org/packages/71/2e/7b1c769803121b809112cf9a00681c472eae1d80e32d7ec0e0bd61d0d0e1/tornado-6.5.7-cp39-abi3-win_arm64.whl", hash = "sha256:ff934fce95643af5f11efdae618eaa73d469dc588641e5c8d19295a0c65c4796", size = 450506, upload-time = "2026-06-08T17:34:49.702Z" }, +version = "6.5.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/d3/343e5bb989d6515b1646cf3d40135d73f3d5e45339bded401b56cdac24dd/tornado-6.5.8.tar.gz", hash = "sha256:9452e1b208a8bd771e2cb1f2ff564985b9b214bdebbe622793e1799e0a6bd23f", size = 520493, upload-time = "2026-08-07T02:12:42.971Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/d5/007086fd8df5489338e204f65adce33fd4f21a4999dbb2b9cff2f897b5f4/tornado-6.5.8-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:cc6aa787d7cfab7c3d35189dc7a56fbd2399a569624c730c6b55b3d6531d0403", size = 449487, upload-time = "2026-08-07T02:12:28.682Z" }, + { url = "https://files.pythonhosted.org/packages/70/c8/5a24a99495903f594f6a199dd7beead1cbc0a13e2cb9102727bcaaf2a997/tornado-6.5.8-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9715b5eb79735b2bcd454ce216a9275b7c0470e64ea1bf5742f78b2f72b26eeb", size = 447649, upload-time = "2026-08-07T02:12:30.306Z" }, + { url = "https://files.pythonhosted.org/packages/6e/de/f2e733f386b85962d1b1dc82cd63d169b5b4580062b35397eac9244a41fe/tornado-6.5.8-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:547d63f450d570c14fe0e8db2cfb14c9bbd1c2503b4a6612586267955aa47b58", size = 450707, upload-time = "2026-08-07T02:12:31.95Z" }, + { url = "https://files.pythonhosted.org/packages/0b/94/20efeee9a01c141e9ac47c397f81679dfda24b32768fc4fff24e76d36c2c/tornado-6.5.8-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e2360a0ffbe145eca8af0b19cb7203d79b1a98dd4cccdd6b368f6f49c2e3808", size = 451677, upload-time = "2026-08-07T02:12:33.512Z" }, + { url = "https://files.pythonhosted.org/packages/42/ec/a96ccb8ccf0de2b7bc2c5fa1608a4803735018242e90c4882365a9fd418f/tornado-6.5.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5d242290bdf7ab3151bc1065fdd75c0dcc21cbc7b49f22a4c56329c2d6566d22", size = 451510, upload-time = "2026-08-07T02:12:35.346Z" }, + { url = "https://files.pythonhosted.org/packages/29/b5/93185859245ad3f00e62175f29607346788b696369347f0146e0421286bb/tornado-6.5.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7b94ff0e128fe0542f3bd331fb44d06260fc4ac16881545159f34ef08aad4195", size = 450917, upload-time = "2026-08-07T02:12:36.963Z" }, + { url = "https://files.pythonhosted.org/packages/97/cf/fe33cf062834487d34d1559746a4a12521033c22645b6d74d4bca702e018/tornado-6.5.8-cp39-abi3-win32.whl", hash = "sha256:67832909c4779c64942380cb5f044a5c6163d00831472d80e25e115de9917836", size = 451952, upload-time = "2026-08-07T02:12:38.512Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e1/468ad54333e92ccb62627e62cb88e5fc14a2171daa67ed47b1b8542d5b86/tornado-6.5.8-cp39-abi3-win_amd64.whl", hash = "sha256:11881db6b7c168494be2c2d12e65931451bdf7ee718535418ae1d8855dd5a0ee", size = 452391, upload-time = "2026-08-07T02:12:39.971Z" }, + { url = "https://files.pythonhosted.org/packages/ad/3e/cd5e4f06e34cde33b8ef66cf36aa2b5ad46354cc1af7d2136bbe365fee1d/tornado-6.5.8-cp39-abi3-win_arm64.whl", hash = "sha256:68a7468c7e289f8514d7d664101753903217eff1bb6822c6b5994a0b5f5bcb26", size = 451411, upload-time = "2026-08-07T02:12:41.469Z" }, ] [[package]] From 541c023bc1da88f6a3f96f0698508baec3e70b64 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:24:23 +0000 Subject: [PATCH 02/13] chore(deps): bump nltk from 3.9.4 to 3.10.3 in /envs/textarena_env Bumps [nltk](https://github.com/nltk/nltk) from 3.9.4 to 3.10.3. - [Release notes](https://github.com/nltk/nltk/releases) - [Changelog](https://github.com/nltk/nltk/blob/develop/ChangeLog) - [Commits](https://github.com/nltk/nltk/compare/3.9.4...v3.10.3) --- updated-dependencies: - dependency-name: nltk dependency-version: 3.10.3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- envs/textarena_env/uv.lock | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/envs/textarena_env/uv.lock b/envs/textarena_env/uv.lock index 496a7c2151..4c7096b200 100644 --- a/envs/textarena_env/uv.lock +++ b/envs/textarena_env/uv.lock @@ -668,6 +668,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b4/bd/05055d8360cef0757d79367157f3b15c0a0715e81e08f86a04018ec045f0/cyclopts-4.10.2-py3-none-any.whl", hash = "sha256:a1f2d6f8f7afac9456b48f75a40b36658778ddc9c6d406b520d017ae32c990fe", size = 204314, upload-time = "2026-04-08T23:57:46.969Z" }, ] +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + [[package]] name = "distro" version = "1.9.0" @@ -1403,17 +1412,18 @@ wheels = [ [[package]] name = "nltk" -version = "3.9.4" +version = "3.10.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, + { name = "defusedxml" }, { name = "joblib" }, { name = "regex" }, { name = "tqdm" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/e6/fe51d2bb1a3b446f59c5c8165999a9fee208bc346af90a7cbf7657bc0d75/nltk-3.10.3.tar.gz", hash = "sha256:bb9327a461c3811c2fa4900e03840401f2126adfb30c0072827c433bd2444ea4", size = 5137152, upload-time = "2026-08-12T23:46:37.258Z" } wheels = [ - { 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" }, + { url = "https://files.pythonhosted.org/packages/b6/6d/ebd2af4640b12168fdf0cb74b6118df2f32a2f62ec7e0c06fbfd80706639/nltk-3.10.3-py3-none-any.whl", hash = "sha256:ff9598a8e20518ee0d557745890cc4435b9578489e2dcbc69c4f81fa060caf7c", size = 1798643, upload-time = "2026-08-12T23:44:13.478Z" }, ] [[package]] @@ -1775,10 +1785,10 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "python-dateutil", marker = "python_full_version < '3.11'" }, - { name = "pytz", marker = "python_full_version < '3.11'" }, - { name = "tzdata", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ @@ -1847,9 +1857,9 @@ resolution-markers = [ "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, - { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" } }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" } wheels = [ @@ -2736,8 +2746,8 @@ name = "secretstorage" version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, - { name = "jeepney", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "cryptography" }, + { name = "jeepney" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } wheels = [ From 7321d7334bab13e87f3982fa0a0b738d069f5e1b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:24:30 +0000 Subject: [PATCH 03/13] chore(deps): bump pypdf from 6.14.2 to 6.16.1 in /envs/repl_env Bumps [pypdf](https://github.com/py-pdf/pypdf) from 6.14.2 to 6.16.1. - [Release notes](https://github.com/py-pdf/pypdf/releases) - [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md) - [Commits](https://github.com/py-pdf/pypdf/compare/6.14.2...6.16.1) --- updated-dependencies: - dependency-name: pypdf dependency-version: 6.16.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- envs/repl_env/uv.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/envs/repl_env/uv.lock b/envs/repl_env/uv.lock index 288cff329c..8d18b00e94 100644 --- a/envs/repl_env/uv.lock +++ b/envs/repl_env/uv.lock @@ -1718,10 +1718,10 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "python-dateutil", marker = "python_full_version < '3.11'" }, - { name = "pytz", marker = "python_full_version < '3.11'" }, - { name = "tzdata", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ @@ -1790,9 +1790,9 @@ resolution-markers = [ "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, - { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" } }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2e/0c/b28ed414f080ee0ad153f848586d61d1878f91689950f037f976ce15f6c8/pandas-3.0.1.tar.gz", hash = "sha256:4186a699674af418f655dbd420ed87f50d56b4cd6603784279d9eef6627823c8", size = 4641901, upload-time = "2026-02-17T22:20:16.434Z" } wheels = [ @@ -2193,14 +2193,14 @@ crypto = [ [[package]] name = "pypdf" -version = "6.14.2" +version = "6.16.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/03/72/7dfd5ff1c9c37de97a731701f51af091325f123d9d4270361c9c69e4431f/pypdf-6.14.2.tar.gz", hash = "sha256:7873f502fe4385e79539b21d872392dc0c4e3714327c15881cbc7fbfd1f95b25", size = 6491182, upload-time = "2026-06-23T14:18:30.859Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/5a/df92d1c1ef8806ca28f20f978ee059894868d93de797a7e2edebe7fe1a43/pypdf-6.16.1.tar.gz", hash = "sha256:c4d1b43ddae921387321cf63936cd16a7743b91d2da92f165c149a195c972ba9", size = 7003737, upload-time = "2026-08-14T12:24:04.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/e6/136aa8993a2ae7214e0b0ef2edaa0d2e08d1d4e4982635b08a835ff31ec8/pypdf-6.14.2-py3-none-any.whl", hash = "sha256:3f07891af76dc002657e04993ab9b4de81de29f9013b9761d0b7968bff12e946", size = 349514, upload-time = "2026-06-23T14:18:28.867Z" }, + { url = "https://files.pythonhosted.org/packages/33/a1/724b18d6757ab7253a8fecd3a430eb8d980ed26872ba16651e7b5ddfc63f/pypdf-6.16.1-py3-none-any.whl", hash = "sha256:63fec31c4092ae50b6729beedcb469055b60d20c834bde1c402df241f371f644", size = 382924, upload-time = "2026-08-14T12:24:02.854Z" }, ] [[package]] @@ -2572,8 +2572,8 @@ name = "secretstorage" version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, - { name = "jeepney", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "cryptography" }, + { name = "jeepney" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } wheels = [ From 10d36474345ddaea5cd044b12575e3400bb485b2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 9 Sep 2026 13:03:54 +0000 Subject: [PATCH 04/13] chore(deps): require patched nltk and pypdf Co-authored-by: benjamin.burtenshaw --- envs/repl_env/pyproject.toml | 2 +- envs/repl_env/uv.lock | 2 +- envs/textarena_env/pyproject.toml | 2 +- envs/textarena_env/uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/envs/repl_env/pyproject.toml b/envs/repl_env/pyproject.toml index 1216734aa3..0c1538ed18 100644 --- a/envs/repl_env/pyproject.toml +++ b/envs/repl_env/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ # REPL custom Gradio tab on /web "gradio>=4.0.0", # Document upload support in the Gradio tab (server-side only, not in the sandbox) - "pypdf>=6.14.2", + "pypdf>=6.16.1", ] [project.optional-dependencies] diff --git a/envs/repl_env/uv.lock b/envs/repl_env/uv.lock index 8d18b00e94..fedace86df 100644 --- a/envs/repl_env/uv.lock +++ b/envs/repl_env/uv.lock @@ -1598,7 +1598,7 @@ requires-dist = [ { name = "huggingface-hub", specifier = ">=0.20.0" }, { name = "openenv", specifier = ">=0.2.3" }, { name = "pydantic", specifier = ">=2.0.0" }, - { name = "pypdf", specifier = ">=6.14.2" }, + { name = "pypdf", specifier = ">=6.16.1" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.3" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0.0" }, { name = "requests", specifier = ">=2.31.0" }, diff --git a/envs/textarena_env/pyproject.toml b/envs/textarena_env/pyproject.toml index 206fe49c5c..9c8af63c8a 100644 --- a/envs/textarena_env/pyproject.toml +++ b/envs/textarena_env/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ # "openspiel>=1.0.0", # "smolagents>=1.22.0,<2", "textarena>=0.6.1", - "nltk>=3.9.3", + "nltk>=3.10.3", # For custom Gradio tab (server/gradio_ui.py) when ENABLE_WEB_INTERFACE=true "gradio>=6.15.1", ] diff --git a/envs/textarena_env/uv.lock b/envs/textarena_env/uv.lock index 4c7096b200..5519cfa3b5 100644 --- a/envs/textarena_env/uv.lock +++ b/envs/textarena_env/uv.lock @@ -1663,7 +1663,7 @@ dev = [ requires-dist = [ { name = "fastapi", specifier = ">=0.115.0" }, { name = "gradio", specifier = ">=6.15.1" }, - { name = "nltk", specifier = ">=3.9.3" }, + { name = "nltk", specifier = ">=3.10.3" }, { name = "openenv", specifier = ">=0.2.2" }, { name = "pydantic", specifier = ">=2.0.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.3" }, From 4d2fc5baf57812f8c2863a2c1031048cf7c64df8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 9 Sep 2026 13:20:06 +0000 Subject: [PATCH 05/13] chore(deps): defer nltk update with proxy regression Co-authored-by: benjamin.burtenshaw --- envs/textarena_env/pyproject.toml | 2 +- envs/textarena_env/uv.lock | 36 +++++++++++-------------------- 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/envs/textarena_env/pyproject.toml b/envs/textarena_env/pyproject.toml index 9c8af63c8a..206fe49c5c 100644 --- a/envs/textarena_env/pyproject.toml +++ b/envs/textarena_env/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ # "openspiel>=1.0.0", # "smolagents>=1.22.0,<2", "textarena>=0.6.1", - "nltk>=3.10.3", + "nltk>=3.9.3", # For custom Gradio tab (server/gradio_ui.py) when ENABLE_WEB_INTERFACE=true "gradio>=6.15.1", ] diff --git a/envs/textarena_env/uv.lock b/envs/textarena_env/uv.lock index 5519cfa3b5..496a7c2151 100644 --- a/envs/textarena_env/uv.lock +++ b/envs/textarena_env/uv.lock @@ -668,15 +668,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b4/bd/05055d8360cef0757d79367157f3b15c0a0715e81e08f86a04018ec045f0/cyclopts-4.10.2-py3-none-any.whl", hash = "sha256:a1f2d6f8f7afac9456b48f75a40b36658778ddc9c6d406b520d017ae32c990fe", size = 204314, upload-time = "2026-04-08T23:57:46.969Z" }, ] -[[package]] -name = "defusedxml" -version = "0.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, -] - [[package]] name = "distro" version = "1.9.0" @@ -1412,18 +1403,17 @@ wheels = [ [[package]] name = "nltk" -version = "3.10.3" +version = "3.9.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, - { name = "defusedxml" }, { name = "joblib" }, { name = "regex" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/e6/fe51d2bb1a3b446f59c5c8165999a9fee208bc346af90a7cbf7657bc0d75/nltk-3.10.3.tar.gz", hash = "sha256:bb9327a461c3811c2fa4900e03840401f2126adfb30c0072827c433bd2444ea4", size = 5137152, upload-time = "2026-08-12T23:46:37.258Z" } +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/b6/6d/ebd2af4640b12168fdf0cb74b6118df2f32a2f62ec7e0c06fbfd80706639/nltk-3.10.3-py3-none-any.whl", hash = "sha256:ff9598a8e20518ee0d557745890cc4435b9578489e2dcbc69c4f81fa060caf7c", size = 1798643, upload-time = "2026-08-12T23:44:13.478Z" }, + { 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]] @@ -1663,7 +1653,7 @@ dev = [ requires-dist = [ { name = "fastapi", specifier = ">=0.115.0" }, { name = "gradio", specifier = ">=6.15.1" }, - { name = "nltk", specifier = ">=3.10.3" }, + { name = "nltk", specifier = ">=3.9.3" }, { name = "openenv", specifier = ">=0.2.2" }, { name = "pydantic", specifier = ">=2.0.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.3" }, @@ -1785,10 +1775,10 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, - { name = "python-dateutil" }, - { name = "pytz" }, - { name = "tzdata" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, + { name = "pytz", marker = "python_full_version < '3.11'" }, + { name = "tzdata", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ @@ -1857,9 +1847,9 @@ resolution-markers = [ "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" } }, - { name = "python-dateutil" }, - { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, + { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" } wheels = [ @@ -2746,8 +2736,8 @@ name = "secretstorage" version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography" }, - { name = "jeepney" }, + { name = "cryptography", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "jeepney", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } wheels = [ From 0d68e5ccfa58e9c69eebe8ba7373513d3ef4279c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 15 Sep 2026 09:00:50 +0000 Subject: [PATCH 06/13] fix(discovery): reject control characters in paths Co-authored-by: benjamin.burtenshaw --- docs/source/guides/catalog-discovery.md | 3 +++ src/openenv/discovery/models.py | 8 ++++++-- .../schemas/0.1-draft/catalog.schema.json | 10 +++++----- .../schemas/0.1-draft/declaration.schema.json | 4 ++-- .../0.1-draft/environment-card.schema.json | 4 ++-- tests/discovery/test_catalog_contract.py | 18 +++++++++++------- 6 files changed, 29 insertions(+), 18 deletions(-) diff --git a/docs/source/guides/catalog-discovery.md b/docs/source/guides/catalog-discovery.md index f5b4e34431..906cdf2cec 100644 --- a/docs/source/guides/catalog-discovery.md +++ b/docs/source/guides/catalog-discovery.md @@ -165,6 +165,9 @@ JSON Schema validation is necessary but is not the whole profile contract. The packaged schemas enforce object shape, required fields, relative-path safety, supported literals, conditional artifact/license-evidence presence, and exactly one orchestration interface. Other rules require semantic validation: +The relative-path profile permits printable UTF-8 (including spaces), but +rejects C0/C1 control characters so untrusted metadata cannot forge CLI or log +lines. | Subject | Additional rule | |---------|-----------------| diff --git a/src/openenv/discovery/models.py b/src/openenv/discovery/models.py index 39d47d6489..d6f7145a25 100644 --- a/src/openenv/discovery/models.py +++ b/src/openenv/discovery/models.py @@ -53,7 +53,10 @@ def relative_path(value: str) -> str: return value if ( not value - or "\x00" in value + or any( + ord(character) < 0x20 or 0x7F <= ord(character) <= 0x9F + for character in value + ) or "\\" in value or PurePosixPath(value).is_absolute() or any(part in ("", ".", "..") for part in value.split("/")) @@ -65,7 +68,8 @@ def relative_path(value: str) -> str: # Positive components exclude "." and ".." without lookaround, which some # JSON Schema regex engines do not support. _PATH_COMPONENT_PATTERN = ( - r"(?:[^./\\\x00]|\.[^./\\\x00]|\.\.[^./\\\x00]|\.\.\.)[^/\\\x00]*" + r"(?:[^./\\\x00-\x1f\x7f-\x9f]|\.[^./\\\x00-\x1f\x7f-\x9f]" + r"|\.\.[^./\\\x00-\x1f\x7f-\x9f]|\.\.\.)[^/\\\x00-\x1f\x7f-\x9f]*" ) RelativePath = Annotated[ NonEmpty, diff --git a/src/openenv/discovery/schemas/0.1-draft/catalog.schema.json b/src/openenv/discovery/schemas/0.1-draft/catalog.schema.json index 3b4491bd06..0907cc6ca8 100644 --- a/src/openenv/discovery/schemas/0.1-draft/catalog.schema.json +++ b/src/openenv/discovery/schemas/0.1-draft/catalog.schema.json @@ -55,7 +55,7 @@ "path": { "allOf": [ { - "pattern": "^(?:\\.|(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*(?:/(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*)*)$" + "pattern": "^(?:\\.|(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*(?:/(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*)*)$" } ], "maxLength": 8192, @@ -405,7 +405,7 @@ "path": { "allOf": [ { - "pattern": "^(?:\\.|(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*(?:/(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*)*)$" + "pattern": "^(?:\\.|(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*(?:/(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*)*)$" } ], "maxLength": 8192, @@ -472,7 +472,7 @@ "path": { "allOf": [ { - "pattern": "^(?:\\.|(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*(?:/(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*)*)$" + "pattern": "^(?:\\.|(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*(?:/(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*)*)$" } ], "maxLength": 8192, @@ -510,7 +510,7 @@ "items": { "allOf": [ { - "pattern": "^(?:\\.|(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*(?:/(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*)*)$" + "pattern": "^(?:\\.|(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*(?:/(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*)*)$" } ], "maxLength": 8192, @@ -524,7 +524,7 @@ "root": { "allOf": [ { - "pattern": "^(?:\\.|(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*(?:/(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*)*)$" + "pattern": "^(?:\\.|(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*(?:/(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*)*)$" } ], "maxLength": 8192, diff --git a/src/openenv/discovery/schemas/0.1-draft/declaration.schema.json b/src/openenv/discovery/schemas/0.1-draft/declaration.schema.json index 91576021ae..56aff4e6ec 100644 --- a/src/openenv/discovery/schemas/0.1-draft/declaration.schema.json +++ b/src/openenv/discovery/schemas/0.1-draft/declaration.schema.json @@ -25,7 +25,7 @@ "source": { "allOf": [ { - "pattern": "^(?:\\.|(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*(?:/(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*)*)$" + "pattern": "^(?:\\.|(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*(?:/(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*)*)$" } ], "maxLength": 8192, @@ -104,7 +104,7 @@ { "allOf": [ { - "pattern": "^(?:\\.|(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*(?:/(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*)*)$" + "pattern": "^(?:\\.|(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*(?:/(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*)*)$" } ], "maxLength": 8192, diff --git a/src/openenv/discovery/schemas/0.1-draft/environment-card.schema.json b/src/openenv/discovery/schemas/0.1-draft/environment-card.schema.json index 0f49575919..861d1bb791 100644 --- a/src/openenv/discovery/schemas/0.1-draft/environment-card.schema.json +++ b/src/openenv/discovery/schemas/0.1-draft/environment-card.schema.json @@ -49,7 +49,7 @@ "path": { "allOf": [ { - "pattern": "^(?:\\.|(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*(?:/(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*)*)$" + "pattern": "^(?:\\.|(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*(?:/(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*)*)$" } ], "maxLength": 8192, @@ -97,7 +97,7 @@ "path": { "allOf": [ { - "pattern": "^(?:\\.|(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*(?:/(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*)*)$" + "pattern": "^(?:\\.|(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*(?:/(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*)*)$" } ], "maxLength": 8192, diff --git a/tests/discovery/test_catalog_contract.py b/tests/discovery/test_catalog_contract.py index f975775264..f4194c602d 100644 --- a/tests/discovery/test_catalog_contract.py +++ b/tests/discovery/test_catalog_contract.py @@ -15,6 +15,10 @@ REVISION = "a" * 40 +ASCII_CONTROL_PATHS = [ + f"envs/control-{chr(codepoint)}" + for codepoint in [*range(0x20), *range(0x7F, 0xA0)] +] @pytest.fixture @@ -92,7 +96,13 @@ def test_tool_declaration_cannot_borrow_another_revision(card): "envs//echo", "envs/echo/", "envs/./echo", - "envs/\x00echo", + *ASCII_CONTROL_PATHS, + "envs/trailing\n", + ".\n", + "..\n", + "envs/\n", + "envs/.\n", + "envs/..\n", ], ) def test_environment_locator_is_a_safe_repository_relative_path( @@ -115,12 +125,6 @@ def test_environment_locator_is_a_safe_repository_relative_path( "envs/...", "envs/a..b", "envs/with spaces", - "envs/trailing\n", - ".\n", - "..\n", - "envs/\n", - "envs/.\n", - "envs/..\n", ], ) def test_schema_and_model_preserve_valid_relative_locators(card, path, card_schema): From b35fcea728af7a18202c64452faf516c1ebf7375 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 15 Sep 2026 09:02:00 +0000 Subject: [PATCH 07/13] fix(discovery): enforce schema control exclusion Co-authored-by: benjamin.burtenshaw --- src/openenv/discovery/models.py | 2 ++ .../schemas/0.1-draft/catalog.schema.json | 25 +++++++++++++++++++ .../schemas/0.1-draft/declaration.schema.json | 10 ++++++++ .../0.1-draft/environment-card.schema.json | 10 ++++++++ tests/discovery/test_catalog_contract.py | 3 +-- 5 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/openenv/discovery/models.py b/src/openenv/discovery/models.py index d6f7145a25..4f25ff85e5 100644 --- a/src/openenv/discovery/models.py +++ b/src/openenv/discovery/models.py @@ -67,6 +67,7 @@ def relative_path(value: str) -> str: # Positive components exclude "." and ".." without lookaround, which some # JSON Schema regex engines do not support. +_CONTROL_CHARACTER_PATTERN = r"[\x00-\x1f\x7f-\x9f]" _PATH_COMPONENT_PATTERN = ( r"(?:[^./\\\x00-\x1f\x7f-\x9f]|\.[^./\\\x00-\x1f\x7f-\x9f]" r"|\.\.[^./\\\x00-\x1f\x7f-\x9f]|\.\.\.)[^/\\\x00-\x1f\x7f-\x9f]*" @@ -83,6 +84,7 @@ def relative_path(value: str) -> str: rf"(?:/{_PATH_COMPONENT_PATTERN})*)$" ), }, + {"not": {"pattern": _CONTROL_CHARACTER_PATTERN}}, ] } ), diff --git a/src/openenv/discovery/schemas/0.1-draft/catalog.schema.json b/src/openenv/discovery/schemas/0.1-draft/catalog.schema.json index 0907cc6ca8..93114d0d77 100644 --- a/src/openenv/discovery/schemas/0.1-draft/catalog.schema.json +++ b/src/openenv/discovery/schemas/0.1-draft/catalog.schema.json @@ -56,6 +56,11 @@ "allOf": [ { "pattern": "^(?:\\.|(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*(?:/(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*)*)$" + }, + { + "not": { + "pattern": "[\\x00-\\x1f\\x7f-\\x9f]" + } } ], "maxLength": 8192, @@ -406,6 +411,11 @@ "allOf": [ { "pattern": "^(?:\\.|(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*(?:/(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*)*)$" + }, + { + "not": { + "pattern": "[\\x00-\\x1f\\x7f-\\x9f]" + } } ], "maxLength": 8192, @@ -473,6 +483,11 @@ "allOf": [ { "pattern": "^(?:\\.|(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*(?:/(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*)*)$" + }, + { + "not": { + "pattern": "[\\x00-\\x1f\\x7f-\\x9f]" + } } ], "maxLength": 8192, @@ -511,6 +526,11 @@ "allOf": [ { "pattern": "^(?:\\.|(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*(?:/(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*)*)$" + }, + { + "not": { + "pattern": "[\\x00-\\x1f\\x7f-\\x9f]" + } } ], "maxLength": 8192, @@ -525,6 +545,11 @@ "allOf": [ { "pattern": "^(?:\\.|(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*(?:/(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*)*)$" + }, + { + "not": { + "pattern": "[\\x00-\\x1f\\x7f-\\x9f]" + } } ], "maxLength": 8192, diff --git a/src/openenv/discovery/schemas/0.1-draft/declaration.schema.json b/src/openenv/discovery/schemas/0.1-draft/declaration.schema.json index 56aff4e6ec..5c81448374 100644 --- a/src/openenv/discovery/schemas/0.1-draft/declaration.schema.json +++ b/src/openenv/discovery/schemas/0.1-draft/declaration.schema.json @@ -26,6 +26,11 @@ "allOf": [ { "pattern": "^(?:\\.|(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*(?:/(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*)*)$" + }, + { + "not": { + "pattern": "[\\x00-\\x1f\\x7f-\\x9f]" + } } ], "maxLength": 8192, @@ -105,6 +110,11 @@ "allOf": [ { "pattern": "^(?:\\.|(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*(?:/(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*)*)$" + }, + { + "not": { + "pattern": "[\\x00-\\x1f\\x7f-\\x9f]" + } } ], "maxLength": 8192, diff --git a/src/openenv/discovery/schemas/0.1-draft/environment-card.schema.json b/src/openenv/discovery/schemas/0.1-draft/environment-card.schema.json index 861d1bb791..b0102491c4 100644 --- a/src/openenv/discovery/schemas/0.1-draft/environment-card.schema.json +++ b/src/openenv/discovery/schemas/0.1-draft/environment-card.schema.json @@ -50,6 +50,11 @@ "allOf": [ { "pattern": "^(?:\\.|(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*(?:/(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*)*)$" + }, + { + "not": { + "pattern": "[\\x00-\\x1f\\x7f-\\x9f]" + } } ], "maxLength": 8192, @@ -98,6 +103,11 @@ "allOf": [ { "pattern": "^(?:\\.|(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*(?:/(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*)*)$" + }, + { + "not": { + "pattern": "[\\x00-\\x1f\\x7f-\\x9f]" + } } ], "maxLength": 8192, diff --git a/tests/discovery/test_catalog_contract.py b/tests/discovery/test_catalog_contract.py index f4194c602d..d5ff0289c1 100644 --- a/tests/discovery/test_catalog_contract.py +++ b/tests/discovery/test_catalog_contract.py @@ -16,8 +16,7 @@ REVISION = "a" * 40 ASCII_CONTROL_PATHS = [ - f"envs/control-{chr(codepoint)}" - for codepoint in [*range(0x20), *range(0x7F, 0xA0)] + f"envs/control-{chr(codepoint)}" for codepoint in [*range(0x20), *range(0x7F, 0xA0)] ] From d6d61f4ad493a42f39656101a9dd1b3617941096 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 15 Sep 2026 09:06:18 +0000 Subject: [PATCH 08/13] docs(discovery): name rejected control ranges Co-authored-by: benjamin.burtenshaw --- docs/source/guides/catalog-discovery.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/guides/catalog-discovery.md b/docs/source/guides/catalog-discovery.md index 906cdf2cec..d5a8862231 100644 --- a/docs/source/guides/catalog-discovery.md +++ b/docs/source/guides/catalog-discovery.md @@ -166,8 +166,8 @@ The packaged schemas enforce object shape, required fields, relative-path safety, supported literals, conditional artifact/license-evidence presence, and exactly one orchestration interface. Other rules require semantic validation: The relative-path profile permits printable UTF-8 (including spaces), but -rejects C0/C1 control characters so untrusted metadata cannot forge CLI or log -lines. +rejects C0, DEL, and C1 control characters so untrusted metadata cannot forge +CLI or log lines. | Subject | Additional rule | |---------|-----------------| From 440faf55e924d32e4ac14ea1988adc9cd44ffd3e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 15 Sep 2026 09:19:06 +0000 Subject: [PATCH 09/13] fix(client): drain dropped sockets before reconnect Co-authored-by: benjamin.burtenshaw --- src/openenv/core/env_client.py | 50 ++++++++--- tests/test_core/test_generic_client.py | 118 +++++++++++++++++++------ 2 files changed, 131 insertions(+), 37 deletions(-) diff --git a/src/openenv/core/env_client.py b/src/openenv/core/env_client.py index f15df6cb2f..f01d9024b6 100644 --- a/src/openenv/core/env_client.py +++ b/src/openenv/core/env_client.py @@ -538,6 +538,13 @@ async def _connect_async(self) -> "EnvClient": self._ws = None self._ws_loop = None + # A timed-out request drops its socket immediately but closes it in the + # background so the timeout itself remains prompt. Wait for that close + # before opening a replacement: the old server-side session continues + # occupying a capacity slot until the close handshake finishes, and + # many environments allow only one session. + await self._drain_pending_close_tasks() + try: self._start_provider_if_needed() except Exception: @@ -578,6 +585,10 @@ async def _disconnect_async(self) -> None: if self._ws is not None: ws = self._ws ws_loop = self._ws_loop + # Detach first so cancellation during the close handshake cannot + # leave a stale socket cached for a later operation. + self._ws = None + self._ws_loop = None same_loop = ws_loop is asyncio.get_running_loop() try: if same_loop: @@ -589,8 +600,23 @@ async def _disconnect_async(self) -> None: await ws.close() except Exception: pass - self._ws = None - self._ws_loop = None + + async def _drain_pending_close_tasks(self) -> None: + """Wait for background socket closes owned by the current event loop. + + Shielding keeps cancellation of the caller from cancelling the close + tasks themselves. This matters both before reconnecting, when the old + server session must release its capacity slot, and during explicit + client shutdown. + """ + loop = asyncio.get_running_loop() + tasks = [ + task + for task in tuple(self._pending_close_tasks) + if not task.done() and task.get_loop() is loop + ] + if tasks: + await asyncio.shield(asyncio.gather(*tasks, return_exceptions=True)) async def _ensure_connected(self) -> None: """Ensure WebSocket connection is established on the current loop. @@ -963,16 +989,16 @@ async def _close_async(self) -> None: self._child_clients.clear() try: - # Wait out any backgrounded closes from a dropped socket (see - # `_receive()` / `_best_effort_close`) so a real close() call still - # sees the handshake through. SyncEnvClient.close() waits for - # `_close_async()` before stopping its loop, so the relevant risk - # is async-context cancellation of close itself — not `_stop_loop()`. - # Keep this gather inside the provider-teardown try/finally so a - # cancelled close cannot skip container/process cleanup. - if self._pending_close_tasks: - await asyncio.gather(*self._pending_close_tasks, return_exceptions=True) - await self._disconnect_async() + try: + # A real close waits out backgrounded closes, but shield them + # from cancellation so their socket handshakes aren't + # abandoned midway. + await self._drain_pending_close_tasks() + finally: + # Run even when pending-close draining is cancelled. A client + # may already have reconnected, and that current socket must + # not remain cached or open during teardown. + await self._disconnect_async() finally: try: if self._provider is not None: diff --git a/tests/test_core/test_generic_client.py b/tests/test_core/test_generic_client.py index 85e07fa227..461eec0918 100644 --- a/tests/test_core/test_generic_client.py +++ b/tests/test_core/test_generic_client.py @@ -17,7 +17,6 @@ import asyncio import os -from contextlib import suppress from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest @@ -1600,17 +1599,64 @@ async def close(self): ) @pytest.mark.asyncio - async def test_close_async_cancelled_during_pending_gather_still_stops_provider( - self, - ): - """Cancelling `_close_async` while draining pending closes must still - tear down the provider and clear provider-owned URLs. + async def test_reconnect_waits_for_dropped_socket_to_release_capacity(self): + """A replacement connection must wait for the old session to close. - Regression: the pending-close `gather` used to run before the - try/finally that stops the provider. Cancellation of `_close_async` - itself propagates from `gather` even with `return_exceptions=True`, - which skipped provider teardown and leaked the container/process. + A timed-out socket is detached immediately and closed in the background. + Reconnecting before that handshake finishes races the server's session + accounting; with the default capacity of one, the retry is rejected. """ + close_started = asyncio.Event() + release_close = asyncio.Event() + + class SlowClose: + state = State.OPEN + + async def send(self, _message): + pass + + async def recv(self): + await asyncio.sleep(10) + + async def close(self): + close_started.set() + await release_close.wait() + self.state = State.CLOSED + + client = GenericEnvClient( + base_url="http://localhost:8000", message_timeout_s=0.01 + ) + dropped_ws = SlowClose() + client._ws = dropped_ws + client._ws_loop = asyncio.get_running_loop() + + with pytest.raises(asyncio.TimeoutError): + await client._send_and_receive({"type": "state"}) + await close_started.wait() + + replacement_ws = AsyncMock() + replacement_ws.state = State.OPEN + replacement_ws.recv.return_value = '{"type": "state", "data": {}}' + + with patch( + "openenv.core.env_client.ws_connect", return_value=replacement_ws + ) as mock_connect: + reconnect = asyncio.create_task(client._connect_async()) + await asyncio.sleep(0) + assert not reconnect.done() + mock_connect.assert_not_called() + + release_close.set() + await reconnect + + mock_connect.assert_called_once() + assert dropped_ws.state == State.CLOSED + assert client._ws is replacement_ws + await client._close_async() + + @pytest.mark.asyncio + async def test_cancelled_close_still_closes_current_and_pending_sockets(self): + """Cancellation while draining an old socket must not leak either one.""" class FakeRuntimeProvider: def __init__(self): @@ -1619,37 +1665,59 @@ def __init__(self): def stop(self): self.stopped = True + close_started = asyncio.Event() + release_close = asyncio.Event() + + class PendingSocket: + state = State.OPEN + + async def close(self): + close_started.set() + await release_close.wait() + self.state = State.CLOSED + + class CurrentSocket: + state = State.OPEN + + async def send(self, _message): + pass + + async def close(self): + self.state = State.CLOSED + provider = FakeRuntimeProvider() client = GenericEnvClient(provider=provider) client._base_url = "http://localhost:8000" client._ws_url = "ws://localhost:8000/ws" - hang_gate = asyncio.Event() - - async def hang_forever(): - await hang_gate.wait() - - pending = asyncio.create_task(hang_forever()) + dropped_ws = PendingSocket() + pending = asyncio.create_task(dropped_ws.close()) client._pending_close_tasks.add(pending) + pending.add_done_callback(client._pending_close_tasks.discard) + await close_started.wait() + + current_ws = CurrentSocket() + client._ws = current_ws + client._ws_loop = asyncio.get_running_loop() close_task = asyncio.create_task(client._close_async()) - await asyncio.sleep(0) # let close enter the pending-close gather + await asyncio.sleep(0) # let close enter the shielded pending-close drain assert not close_task.done() close_task.cancel() with pytest.raises(asyncio.CancelledError): await close_task - hang_gate.set() - with suppress(asyncio.CancelledError): - await pending - - assert provider.stopped, ( - "provider.stop() must run even when _close_async is cancelled " - "during the pending-close gather" - ) + assert provider.stopped assert client._base_url is None assert client._ws_url is None + assert client._ws is None + assert current_ws.state == State.CLOSED + assert not pending.cancelled() + + release_close.set() + await pending + assert dropped_ws.state == State.CLOSED # ============================================================================ From 39e30dc9d587a58373a585b458445a521f102db2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 15 Sep 2026 09:19:45 +0000 Subject: [PATCH 10/13] test(client): await mocked reconnect correctly Co-authored-by: benjamin.burtenshaw --- tests/test_core/test_generic_client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_core/test_generic_client.py b/tests/test_core/test_generic_client.py index 461eec0918..b297424401 100644 --- a/tests/test_core/test_generic_client.py +++ b/tests/test_core/test_generic_client.py @@ -1638,8 +1638,11 @@ async def close(self): replacement_ws.state = State.OPEN replacement_ws.recv.return_value = '{"type": "state", "data": {}}' + async def fake_ws_connect(*args, **kwargs): + return replacement_ws + with patch( - "openenv.core.env_client.ws_connect", return_value=replacement_ws + "openenv.core.env_client.ws_connect", side_effect=fake_ws_connect ) as mock_connect: reconnect = asyncio.create_task(client._connect_async()) await asyncio.sleep(0) From 2831ed5a42adb19934f76e78694a124b4acc5b37 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 15 Sep 2026 10:06:14 +0000 Subject: [PATCH 11/13] fix(discovery): ignore relative XDG cache paths Co-authored-by: benjamin.burtenshaw --- src/openenv/auto/_discovery.py | 4 +++- tests/envs/test_discovery.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/openenv/auto/_discovery.py b/src/openenv/auto/_discovery.py index 7b53fc6bcc..2f7a57dbdb 100644 --- a/src/openenv/auto/_discovery.py +++ b/src/openenv/auto/_discovery.py @@ -345,9 +345,11 @@ def _default_cache_file() -> Path: shared, world-writable temporary directory. A fixed path under the shared temp dir lets another local user pre-create the cache file and redirect discovery to attacker-controlled import paths (`import_module` on a cached `client_module_path`). + Per the XDG Base Directory specification, relative `XDG_CACHE_HOME` values are + ignored so an untrusted working tree cannot supply a victim-owned cache file. """ base = os.environ.get("XDG_CACHE_HOME") - root = Path(base) if base else Path.home() / ".cache" + root = Path(base) if base and Path(base).is_absolute() else Path.home() / ".cache" return root / "openenv" / "discovery_cache.json" diff --git a/tests/envs/test_discovery.py b/tests/envs/test_discovery.py index ff1d3890bd..08552e5b0f 100644 --- a/tests/envs/test_discovery.py +++ b/tests/envs/test_discovery.py @@ -16,6 +16,7 @@ import os import stat import tempfile +from pathlib import Path from unittest.mock import Mock, patch import openenv.auto._discovery as _discovery_module @@ -344,6 +345,24 @@ def test_cache_file_is_per_user_not_shared_tmp(self): assert tempfile.gettempdir() not in str(path) assert path.parent.name == "openenv" + def test_relative_xdg_cache_home_cannot_redirect_into_working_tree( + self, tmp_path, monkeypatch + ): + """A relative XDG path must not trust a cache planted in the checkout.""" + checkout = tmp_path / "untrusted-checkout" + planted = checkout / "cache" / "openenv" / "discovery_cache.json" + planted.parent.mkdir(parents=True) + planted.write_text("{}") + + monkeypatch.chdir(checkout) + monkeypatch.setenv("XDG_CACHE_HOME", "cache") + + path = _default_cache_file() + + assert path == Path.home() / ".cache" / "openenv" / "discovery_cache.json" + assert path.is_absolute() + assert path.resolve() != planted.resolve() + def test_world_writable_cache_is_not_trusted(self, tmp_path): f = tmp_path / "cache.json" f.write_text("{}") From 7dc8031ca10765804a23d673c2c52035dbc403d0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 16 Sep 2026 06:19:10 +0000 Subject: [PATCH 12/13] fix(client): track cancelled disconnect handshakes Co-authored-by: benjamin.burtenshaw --- src/openenv/core/env_client.py | 39 +++++++++++++------- tests/test_core/test_generic_client.py | 51 ++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 13 deletions(-) diff --git a/src/openenv/core/env_client.py b/src/openenv/core/env_client.py index f01d9024b6..ec60510ba6 100644 --- a/src/openenv/core/env_client.py +++ b/src/openenv/core/env_client.py @@ -255,6 +255,15 @@ async def _best_effort_close(ws: ClientConnection) -> None: pass # Best effort +async def _best_effort_disconnect(ws: ClientConnection) -> None: + """Notify the server, then close the socket without propagating failures.""" + try: + await ws.send(json.dumps({"type": "close"})) + except (Exception, asyncio.CancelledError): + pass # Best effort + await _best_effort_close(ws) + + class EnvClient(ABC, Generic[ActT, ObsT, StateT]): """ Async environment client for persistent sessions. @@ -580,6 +589,16 @@ async def _connect_async(self) -> "EnvClient": def disconnect(self) -> Any: return self._dispatch(self._disconnect_async) + def _schedule_socket_close( + self, ws: ClientConnection, *, notify_server: bool = False + ) -> asyncio.Task[None]: + """Schedule and track a socket close on the current event loop.""" + close = _best_effort_disconnect(ws) if notify_server else _best_effort_close(ws) + close_task = asyncio.create_task(close) + self._pending_close_tasks.add(close_task) + close_task.add_done_callback(self._pending_close_tasks.discard) + return close_task + async def _disconnect_async(self) -> None: """Close the WebSocket connection.""" if self._ws is not None: @@ -590,16 +609,12 @@ async def _disconnect_async(self) -> None: self._ws = None self._ws_loop = None same_loop = ws_loop is asyncio.get_running_loop() - try: - if same_loop: - await ws.send(json.dumps({"type": "close"})) - except Exception: - pass # Best effort - try: - if same_loop: - await ws.close() - except Exception: - pass + if same_loop: + # Track the detached socket before awaiting anything. If this + # caller is cancelled, the shielded task keeps closing and a + # later reconnect drains it before opening a replacement. + close_task = self._schedule_socket_close(ws, notify_server=True) + await asyncio.shield(close_task) async def _drain_pending_close_tasks(self) -> None: """Wait for background socket closes owned by the current event loop. @@ -667,9 +682,7 @@ async def _receive(self) -> Dict[str, Any]: # would actually block for up to 10s before its deadline was # honored. Scheduling it lets the exception propagate # immediately while the close still happens in the background. - close_task = asyncio.ensure_future(_best_effort_close(ws)) - self._pending_close_tasks.add(close_task) - close_task.add_done_callback(self._pending_close_tasks.discard) + self._schedule_socket_close(ws) raise return json.loads(raw) diff --git a/tests/test_core/test_generic_client.py b/tests/test_core/test_generic_client.py index 3fc790cc02..9eee8413b4 100644 --- a/tests/test_core/test_generic_client.py +++ b/tests/test_core/test_generic_client.py @@ -1720,6 +1720,57 @@ async def fake_ws_connect(*args, **kwargs): assert client._ws is replacement_ws await client._close_async() + @pytest.mark.asyncio + async def test_cancelled_disconnect_drains_current_socket_before_reconnect(self): + """A cancelled disconnect must keep tracking the detached socket.""" + close_started = asyncio.Event() + release_close = asyncio.Event() + + class SlowClose: + state = State.OPEN + + async def send(self, _message): + pass + + async def close(self): + close_started.set() + await release_close.wait() + self.state = State.CLOSED + + client = GenericEnvClient(base_url="http://localhost:8000") + current_ws = SlowClose() + client._ws = current_ws + client._ws_loop = asyncio.get_running_loop() + + disconnect = asyncio.create_task(client._disconnect_async()) + await asyncio.wait_for(close_started.wait(), timeout=1) + disconnect.cancel() + with pytest.raises(asyncio.CancelledError): + await disconnect + + replacement_ws = AsyncMock() + replacement_ws.state = State.OPEN + + async def fake_ws_connect(*args, **kwargs): + return replacement_ws + + with patch( + "openenv.core.env_client.ws_connect", side_effect=fake_ws_connect + ) as mock_connect: + reconnect = asyncio.create_task(client._connect_async()) + await asyncio.sleep(0) + reconnect_waited_for_close = not reconnect.done() + calls_before_close_finished = mock_connect.call_count + + release_close.set() + await asyncio.wait_for(reconnect, timeout=1) + + assert reconnect_waited_for_close + assert calls_before_close_finished == 0 + assert current_ws.state == State.CLOSED + assert client._ws is replacement_ws + await client._close_async() + @pytest.mark.asyncio async def test_cancelled_close_still_closes_current_and_pending_sockets(self): """Cancellation while draining an old socket must not leak either one.""" From f336c19aff33383ce0e1e7819929e55e24bb4d8a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 16 Sep 2026 06:30:11 +0000 Subject: [PATCH 13/13] fix: harden close teardown and relative HOME cache fallback Keep parent WebSocket/provider cleanup running when child.close is cancelled, and refuse relative HOME when selecting the discovery cache so an untrusted checkout cannot plant a victim-owned cache path. Co-authored-by: benjamin.burtenshaw --- src/openenv/auto/_discovery.py | 20 +++++++++- src/openenv/core/env_client.py | 50 +++++++++++++------------ tests/envs/test_discovery.py | 21 +++++++++++ tests/test_core/test_generic_client.py | 52 ++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 25 deletions(-) diff --git a/src/openenv/auto/_discovery.py b/src/openenv/auto/_discovery.py index 2f7a57dbdb..1f7054ded5 100644 --- a/src/openenv/auto/_discovery.py +++ b/src/openenv/auto/_discovery.py @@ -23,6 +23,7 @@ import os import re import stat +import tempfile from dataclasses import asdict, dataclass from pathlib import Path from typing import Any, Type @@ -347,9 +348,26 @@ def _default_cache_file() -> Path: attacker-controlled import paths (`import_module` on a cached `client_module_path`). Per the XDG Base Directory specification, relative `XDG_CACHE_HOME` values are ignored so an untrusted working tree cannot supply a victim-owned cache file. + Relative `HOME` values are likewise rejected: `Path.home()` must not be + resolved against the current working directory. """ base = os.environ.get("XDG_CACHE_HOME") - root = Path(base) if base and Path(base).is_absolute() else Path.home() / ".cache" + if base and Path(base).is_absolute(): + root = Path(base) + else: + home = Path.home() + if home.is_absolute(): + root = home / ".cache" + else: + # Keep the fallback absolute and uid-scoped so a shared temp root + # cannot be turned into a fixed, cross-user planting target. + uid = os.getuid() if hasattr(os, "getuid") else os.getpid() + root = Path(tempfile.gettempdir()) / f"openenv-{uid}-cache" + if not root.is_absolute(): + raise RuntimeError( + "Cannot resolve an absolute discovery cache directory when " + "XDG_CACHE_HOME and HOME are both missing or relative" + ) return root / "openenv" / "discovery_cache.json" diff --git a/src/openenv/core/env_client.py b/src/openenv/core/env_client.py index ec60510ba6..164f99483b 100644 --- a/src/openenv/core/env_client.py +++ b/src/openenv/core/env_client.py @@ -996,34 +996,36 @@ async def _close_async(self) -> None: If this client was created via from_docker_image() or from_env(), this will also stop and remove the associated container/process. """ - for child in list(self._child_clients): - with suppress(Exception): - await child.close() - self._child_clients.clear() - try: - try: - # A real close waits out backgrounded closes, but shield them - # from cancellation so their socket handshakes aren't - # abandoned midway. - await self._drain_pending_close_tasks() - finally: - # Run even when pending-close draining is cancelled. A client - # may already have reconnected, and that current socket must - # not remain cached or open during teardown. - await self._disconnect_async() + for child in list(self._child_clients): + with suppress(Exception): + await child.close() finally: + # Parent teardown must run even when a child close is cancelled. + self._child_clients.clear() try: - if self._provider is not None: - # Handle both ContainerProvider and RuntimeProvider - if hasattr(self._provider, "stop_container"): - self._provider.stop_container() - elif hasattr(self._provider, "stop"): - self._provider.stop() + try: + # A real close waits out backgrounded closes, but shield them + # from cancellation so their socket handshakes aren't + # abandoned midway. + await self._drain_pending_close_tasks() + finally: + # Run even when pending-close draining is cancelled. A client + # may already have reconnected, and that current socket must + # not remain cached or open during teardown. + await self._disconnect_async() finally: - if self._start_provider_on_connect: - self._base_url = None - self._ws_url = None + try: + if self._provider is not None: + # Handle both ContainerProvider and RuntimeProvider + if hasattr(self._provider, "stop_container"): + self._provider.stop_container() + elif hasattr(self._provider, "stop"): + self._provider.stop() + finally: + if self._start_provider_on_connect: + self._base_url = None + self._ws_url = None def _stop_provider_best_effort(self) -> None: """Stop the underlying provider directly, ignoring any errors. diff --git a/tests/envs/test_discovery.py b/tests/envs/test_discovery.py index 08552e5b0f..a54a0703c2 100644 --- a/tests/envs/test_discovery.py +++ b/tests/envs/test_discovery.py @@ -363,6 +363,27 @@ def test_relative_xdg_cache_home_cannot_redirect_into_working_tree( assert path.is_absolute() assert path.resolve() != planted.resolve() + def test_relative_home_cannot_redirect_into_working_tree( + self, tmp_path, monkeypatch + ): + """A relative HOME must not select a cache planted in the checkout.""" + checkout = tmp_path / "untrusted-checkout" + planted = checkout / "cache" / ".cache" / "openenv" / "discovery_cache.json" + planted.parent.mkdir(parents=True) + planted.write_text("{}") + + monkeypatch.chdir(checkout) + monkeypatch.delenv("XDG_CACHE_HOME", raising=False) + monkeypatch.setenv("HOME", "cache") + + path = _default_cache_file() + + assert path.is_absolute() + assert path.resolve() != planted.resolve() + assert "openenv" in path.parts + uid = os.getuid() if hasattr(os, "getuid") else os.getpid() + assert f"openenv-{uid}-cache" in path.parts + def test_world_writable_cache_is_not_trusted(self, tmp_path): f = tmp_path / "cache.json" f.write_text("{}") diff --git a/tests/test_core/test_generic_client.py b/tests/test_core/test_generic_client.py index 9eee8413b4..44196c5422 100644 --- a/tests/test_core/test_generic_client.py +++ b/tests/test_core/test_generic_client.py @@ -1836,6 +1836,58 @@ async def close(self): await pending assert dropped_ws.state == State.CLOSED + @pytest.mark.asyncio + async def test_cancelled_child_close_still_tears_down_parent(self): + """Cancellation during child.close() must not skip parent teardown.""" + + class FakeRuntimeProvider: + def __init__(self): + self.stopped = False + + def stop(self): + self.stopped = True + + child_started = asyncio.Event() + release_child = asyncio.Event() + + class SlowChild: + async def close(self): + child_started.set() + await release_child.wait() + + class ParentSocket: + state = State.OPEN + + async def send(self, _message): + pass + + async def close(self): + self.state = State.CLOSED + + provider = FakeRuntimeProvider() + client = GenericEnvClient(provider=provider) + client._base_url = "http://localhost:8000" + client._ws_url = "ws://localhost:8000/ws" + client._child_clients.append(SlowChild()) + parent_ws = ParentSocket() + client._ws = parent_ws + client._ws_loop = asyncio.get_running_loop() + + close_task = asyncio.create_task(client._close_async()) + await child_started.wait() + close_task.cancel() + with pytest.raises(asyncio.CancelledError): + await close_task + + assert provider.stopped + assert client._base_url is None + assert client._ws_url is None + assert client._ws is None + assert parent_ws.state == State.CLOSED + assert client._child_clients == [] + + release_child.set() + # ============================================================================ # Integration Tests (require running server)