From 7f45652f7d4ae814807562de0b83de9f91714626 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Aug 2026 17:44:28 +0000 Subject: [PATCH 01/47] chore(deps): bump caddy from 2.11.4 to 2.11.4 in the compose group Bumps the compose group with 1 update: caddy. Updates `caddy` from 2.11.4 to 2.11.4 --- updated-dependencies: - dependency-name: caddy dependency-version: 2.11.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: compose ... Signed-off-by: dependabot[bot] --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 38dbd23b..452d097d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -908,7 +908,7 @@ services: # Bridges the local 127.0.0.1 application binding to the LAN # Serves HTTPS or HTTP depending on configuration caddy: - image: caddy:2.11.4@sha256:844f60b64e4724a5aa8245e019dace0d3f199f7433ce6c57676cb30a920dbad9 + image: caddy:2.11.4@sha256:df7f1c2fb114453b951de51a98efc010db1655a92c2e86be6706714e2417a78d container_name: caddy # Memory ceiling (#132 — see monerod). Reverse proxy + local TLS only (~13 MiB observed). mem_limit: 128m From c74b3ba477ba7de716c77f7ad63fe97bfe00253f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Aug 2026 17:45:54 +0000 Subject: [PATCH 02/47] chore(deps): bump the python group in /build/dashboard with 4 updates Updates the requirements on [hypothesis](https://github.com/HypothesisWorks/hypothesis), [ruff](https://github.com/astral-sh/ruff), [pre-commit](https://github.com/pre-commit/pre-commit) and [setuptools](https://github.com/pypa/setuptools) to permit the latest version. Updates `hypothesis` from 6.165.2 to 6.165.9 - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/v6.165.2...v6.165.9) Updates `ruff` from 0.16.2 to 0.16.3 - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.16.2...0.16.3) Updates `pre-commit` from 4.6.1 to 4.6.2 - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v4.6.1...v4.6.2) Updates `setuptools` to 84.0.0 - [Release notes](https://github.com/pypa/setuptools/releases) - [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst) - [Commits](https://github.com/pypa/setuptools/compare/v83.0.0...v84.0.0) --- updated-dependencies: - dependency-name: hypothesis dependency-version: 6.165.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: python - dependency-name: ruff dependency-version: 0.16.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: python - dependency-name: pre-commit dependency-version: 4.6.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: python - dependency-name: setuptools dependency-version: 84.0.0 dependency-type: direct:development dependency-group: python ... Signed-off-by: dependabot[bot] --- build/dashboard/pyproject.toml | 6 +- build/dashboard/uv.lock | 181 ++++++++++++++++++--------------- 2 files changed, 103 insertions(+), 84 deletions(-) diff --git a/build/dashboard/pyproject.toml b/build/dashboard/pyproject.toml index 8efad63c..1bea078c 100644 --- a/build/dashboard/pyproject.toml +++ b/build/dashboard/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=83.0.0"] +requires = ["setuptools>=84.0.0"] build-backend = "setuptools.build_meta" [project] @@ -34,11 +34,11 @@ test = [ # diff-cover (#286): patch-coverage gate — new/changed lines must be >=90% covered. "diff-cover>=10.5.0", # hypothesis (#284): property-based tests asserting invariants on the money/numeric logic. - "hypothesis>=6.165.2", + "hypothesis>=6.165.9", ] # Developer tooling (Wave 7, #280). Pinned so local, pre-commit, and CI all run the SAME ruff # — lint output is version-sensitive, so a floor would let CI and a contributor disagree. -dev = ["ruff==0.16.2", "pre-commit>=4.6.1"] +dev = ["ruff==0.16.3", "pre-commit>=4.6.2"] [tool.setuptools.packages.find] include = ["mining_dashboard*"] diff --git a/build/dashboard/uv.lock b/build/dashboard/uv.lock index b351fc9f..6be4057d 100644 --- a/build/dashboard/uv.lock +++ b/build/dashboard/uv.lock @@ -608,67 +608,86 @@ wheels = [ [[package]] name = "hypothesis" -version = "6.165.2" +version = "6.165.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/73/fc3743243603dc49911a1ec073a3a524ea8e1c7d48218d3c2a3faa9a8709/hypothesis-6.165.2.tar.gz", hash = "sha256:680a1adf523ac792b46064f425b112ce6c08a7a8f50e65d08e029de6aa11df95", size = 502277, upload-time = "2026-08-05T21:32:43.713Z" } +sdist = { url = "https://files.pythonhosted.org/packages/68/a8/f7afb4b55a7f00307f246fb7c3cf2abdc8f8e0ad7eb81bf23f8025480da6/hypothesis-6.165.9.tar.gz", hash = "sha256:a5d1fa312020b499c517e1217ef4440c56a9b52e20b47b420b48a583a81c412b", size = 503594, upload-time = "2026-08-15T02:50:07.518Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/63/46c9908fe7bd5ffa5002fa88fe289dfe6d3cea3fad1ab8942fe11f1c8a2b/hypothesis-6.165.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:33a7303566e660664f3f02ea1df85f7b966cd6723165c696996cfd8630913b3a", size = 781704, upload-time = "2026-08-05T21:32:03.548Z" }, - { url = "https://files.pythonhosted.org/packages/c5/7f/fdce62542a514f6b33c4fc0a760e6d17bb57602b28cb079b78e55b8ea32d/hypothesis-6.165.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:06d8fe4c82a935f67e610c99848360f5caeb04f547c7d7a830a5c74fb96f053a", size = 777243, upload-time = "2026-08-05T21:32:17.546Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c8/39cd922bf3e1ec84977b768d4e8be31ae051e3a6b400b4fdd7ebcddb1eed/hypothesis-6.165.2-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de2e3f6a6f75c876be481138c6c0802ebe10deef9f13ce1cdd6e0ed21d8e1e28", size = 1106492, upload-time = "2026-08-05T21:31:47.844Z" }, - { url = "https://files.pythonhosted.org/packages/0f/91/7bb502379a8dcc43f2538530c05cf16fd4e386afa587d65cc289484425cf/hypothesis-6.165.2-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:21668cb5a8a694d45ff4c43f20a8fc577a47467b5102c680a43638b027136368", size = 1135105, upload-time = "2026-08-05T21:31:49.453Z" }, - { url = "https://files.pythonhosted.org/packages/e5/04/4ce8ae1bf78d09d7543ab7037a3beedc7f3d963c7aa04e82842415284689/hypothesis-6.165.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eea4ab5cfdd6c6a23a60777559ea06c34868234fff6542ff6125c0250429348e", size = 1156034, upload-time = "2026-08-05T21:31:44.687Z" }, - { url = "https://files.pythonhosted.org/packages/49/de/b074d899f4a04fa8b99a5bbd66f209c1c02bc21f9f87404539b28b99aff0/hypothesis-6.165.2-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:0a6add02d9b3b73b59f4d69f5b15abbc07113cd335cc140815cec2877e6b496c", size = 1111344, upload-time = "2026-08-05T21:32:27.211Z" }, - { url = "https://files.pythonhosted.org/packages/e8/38/5a8514683a181f82a8ad9f6d084b704fea7a97c9814033939fc493b55fca/hypothesis-6.165.2-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11013896b6a2ed497079558cb9f89e8b3b564f8859782b38f72cfc1dbeca66ad", size = 1148115, upload-time = "2026-08-05T21:31:01.009Z" }, - { url = "https://files.pythonhosted.org/packages/88/c7/08cf7930d8bec7f1df971c948af2ccb5a402d5b8b19b306af655a603b180/hypothesis-6.165.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4ceabc69a95e761f381663c6452537fde12a2a6e0275e095b6822c0e0f3b1364", size = 1280321, upload-time = "2026-08-05T21:31:14.843Z" }, - { url = "https://files.pythonhosted.org/packages/c5/91/c9ebb7da3b6e06c47aecceb959cf7df6af75025663af543373c5692f97ac/hypothesis-6.165.2-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:11e1ce261765ffa6acbaf540358426519ca4a5e46b825cf39b429c77c1895689", size = 1408134, upload-time = "2026-08-05T21:31:27.383Z" }, - { url = "https://files.pythonhosted.org/packages/ed/24/13c2fd9f253ba3a92d4aaa61ae45a8b130df57ab5bd6fc481e2aae520e36/hypothesis-6.165.2-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:b0250099b2e55d72872319918aacc501110a182938a3d56bcae4a999bee5db08", size = 1280884, upload-time = "2026-08-05T21:32:00.188Z" }, - { url = "https://files.pythonhosted.org/packages/6e/fa/2820bdbe0660394544b9e120b03ea7020702d7fa5c76236166de6ababc9d/hypothesis-6.165.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:96d02928d1a0b7d59e39fd8ada75f0b7d0377ff29f91c94109f92fc2dab9af74", size = 1322998, upload-time = "2026-08-05T21:31:24.373Z" }, - { url = "https://files.pythonhosted.org/packages/f0/24/38752794eb821f5c77da08523602003c344f3498fce09f20741cd5b5e29c/hypothesis-6.165.2-cp310-abi3-win32.whl", hash = "sha256:0f2044093c8244d73893e755a7fa53154b7eca57b37e1427d9b0d9948f6c2b3e", size = 667506, upload-time = "2026-08-05T21:32:10.547Z" }, - { url = "https://files.pythonhosted.org/packages/5f/71/b28f714a127017750e450d152aa4fbff51bd144c6840090d28b529b408d3/hypothesis-6.165.2-cp310-abi3-win_amd64.whl", hash = "sha256:2aa30716066e5ee7750e56b8f90cefaf4ed28c12b4b1d66cef40014fd3f95196", size = 673650, upload-time = "2026-08-05T21:31:25.726Z" }, - { url = "https://files.pythonhosted.org/packages/67/43/ba4e9140480326c517e46f6d16fb2efd423a579f4f50ff79d0326226c563/hypothesis-6.165.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a49889d19484ccfe1977f448a9c0ad27078232a707fa9f9c8667e9d7ffbe792d", size = 782176, upload-time = "2026-08-05T21:32:08.821Z" }, - { url = "https://files.pythonhosted.org/packages/bf/10/f0ed8d6906a959f015bb6140413e5be7965ff50f965478316cc3a7469254/hypothesis-6.165.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67560ca111bcad4e07dc8390a499d9b5c6b5488bab0b3c801a4c31a02238e199", size = 777969, upload-time = "2026-08-05T21:32:31.397Z" }, - { url = "https://files.pythonhosted.org/packages/8a/1e/137cc1cc254c4b00eb41ddebccf0a33cc2ad99f6bb531e68525c2561c475/hypothesis-6.165.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54e5afbdf7c973ced6984265a7ffbcdd3b072349c6e9f9d92af889a5af8d2a08", size = 1106856, upload-time = "2026-08-05T21:30:59.63Z" }, - { url = "https://files.pythonhosted.org/packages/f2/16/52e37b42dfa7579176aa9c8ada2f94eadcd27659b71c85fb477df0d57ad8/hypothesis-6.165.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3aed6b889f493d75b7f734e931d10dc003d7277af63956f8d2f131326aaf297", size = 1156324, upload-time = "2026-08-05T21:31:51.917Z" }, - { url = "https://files.pythonhosted.org/packages/6c/31/467ccd787de1cd77c9a23b963580bbe0ec00e8163ee0df224b3bfcbc2c94/hypothesis-6.165.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:929e47581a4a20b81a67deecd48f6589151fada7c23a418d8505c4595de14577", size = 1280680, upload-time = "2026-08-05T21:31:32.452Z" }, - { url = "https://files.pythonhosted.org/packages/39/9b/6edfb53334df96a0889978374a115fbc1569ffdef7786fe256ed71aa72a1/hypothesis-6.165.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:020a889e548e81514132fe215cca664ee3961a8d1343262c2aa6ea7e5f31370a", size = 1323331, upload-time = "2026-08-05T21:31:13.393Z" }, - { url = "https://files.pythonhosted.org/packages/67/83/92a460ee8811360b5f95cc547d6f43e632457be0bd1a28e497c84e46fa7a/hypothesis-6.165.2-cp311-cp311-win_amd64.whl", hash = "sha256:1bd8955bd5dd72bb9bd6f7243cef8a2bf8a264e23edbd29eecebe41de9348eb9", size = 673376, upload-time = "2026-08-05T21:31:22.637Z" }, - { url = "https://files.pythonhosted.org/packages/98/81/a9039e7eee38523e2ad13e9e4e70c508f25d4205fb4c6ceb98632547ca82/hypothesis-6.165.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b24f1b238deb97fda828a939931de3210f5cef21e87fe0b941fafbeb55ead676", size = 783294, upload-time = "2026-08-05T21:31:56.881Z" }, - { url = "https://files.pythonhosted.org/packages/8d/e5/120642320291d8d117a83491d93527149ddbd15e56399274741fc4bd3a9a/hypothesis-6.165.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:306763ce7186e08ee30dba409b320873d1afc54adf76b44c6bf83b5867d17359", size = 774867, upload-time = "2026-08-05T21:32:05.489Z" }, - { url = "https://files.pythonhosted.org/packages/b5/ef/251607eb2446fb44e8faa83e30aa1b6cc281b6eca5d0ecaabe7527e3395d/hypothesis-6.165.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f878ebc5c33e4e8e8a90bd3d7ccc3f3a7370847aa22243536e673047f0f4c37", size = 1105307, upload-time = "2026-08-05T21:31:53.719Z" }, - { url = "https://files.pythonhosted.org/packages/54/f1/de30869d83f00137a664319a4acb0ba8b1a9e2c879afdc12abb1b4c703f7/hypothesis-6.165.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d726513b32cc6407667ac0812fa3517408f933b89b16b6b84f296335eec18432", size = 1155348, upload-time = "2026-08-05T21:31:58.536Z" }, - { url = "https://files.pythonhosted.org/packages/fa/2c/8925c2bddf6e105d947a04511cd5d536eabc8d4ed926518a0d1672ede007/hypothesis-6.165.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a700c0e193707e3b6f1b23f1d5f534896dd9f79bb2a2340582579bad5f5b59a4", size = 1278124, upload-time = "2026-08-05T21:32:14.049Z" }, - { url = "https://files.pythonhosted.org/packages/b9/00/e285e7987e96d74e229fcb94c58dd8e85290966fc2040fbac4b081fc49c9/hypothesis-6.165.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:50d50e313dfff2e79c754b92a4c88c479dd9e102a56c60caa5b3d6263caa1b02", size = 1322340, upload-time = "2026-08-05T21:31:11.671Z" }, - { url = "https://files.pythonhosted.org/packages/51/7a/990e802222b3a88a284a872fc41339e39d8b619e5266aae45ef1c5da231a/hypothesis-6.165.2-cp312-cp312-win_amd64.whl", hash = "sha256:e2493b71a6e75dbd9ab33f8ab3920a6850a7965de55baf9725738a227ef3bfd2", size = 670805, upload-time = "2026-08-05T21:32:19.278Z" }, - { url = "https://files.pythonhosted.org/packages/22/01/add18f19d5e5f084a59709f0dcebf3cb1edeca475ce8a31573dc33891e66/hypothesis-6.165.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e2bf15d05264ec9da8d55c3843902f906ced5e84fb3da924eafe165697ab4638", size = 783183, upload-time = "2026-08-05T21:31:55.305Z" }, - { url = "https://files.pythonhosted.org/packages/8e/4b/df2e4c24d208518a6a3dab7acabad7f5ec6c5bb0f4d0bf1701d2fb7206ce/hypothesis-6.165.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3748153d4f64d347f8c988dd41fbef3513b66819e73e9209b1c501bf0d716a13", size = 774829, upload-time = "2026-08-05T21:32:01.891Z" }, - { url = "https://files.pythonhosted.org/packages/72/a0/b75a001efbde704ff2188924a4d4bb3cdf7a22924dc51c155115af64858c/hypothesis-6.165.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f689976e0eb578afbe8ce37669cb637f00f7c545ad303412947ee4abe2f29ce6", size = 1105224, upload-time = "2026-08-05T21:32:35.189Z" }, - { url = "https://files.pythonhosted.org/packages/71/f2/4942f510c6441b5d60dc7f0d8d2af74fe444b62d3af565bdf42d9b6b803d/hypothesis-6.165.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7c68a5684b2e2ad3c33500198a6073b3d04493fd7b1ef34937645ad092b797d", size = 1155166, upload-time = "2026-08-05T21:31:46.408Z" }, - { url = "https://files.pythonhosted.org/packages/48/11/405ebff50c6949518d34f487017412d5b8f8ee9239e50ecdfdd99a745f4b/hypothesis-6.165.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b40db922ccb53fb77c68d944748a3eb5b945402967b64093d9ba970d028cf1af", size = 1278170, upload-time = "2026-08-05T21:31:43.116Z" }, - { url = "https://files.pythonhosted.org/packages/73/ff/93ad0f4b55100876604d2c316a8c6e0ee037cb0da925caaa478709e504b0/hypothesis-6.165.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d2fd48ec969b2dbe1c8e25dc86d199c6820b9222846469449b997f5546383378", size = 1322061, upload-time = "2026-08-05T21:32:07.217Z" }, - { url = "https://files.pythonhosted.org/packages/14/37/14b655c664a957e44c7f59d9498e53d79b55f1b752a1bb38fee9da403e9c/hypothesis-6.165.2-cp313-cp313-win_amd64.whl", hash = "sha256:9cf13225121280036ea5a8ff8babb82ec27a4736aea669bbe0bc9839d254575f", size = 670824, upload-time = "2026-08-05T21:32:21.25Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ce/a2de75f1a12b6670edfca890794daab685a63ab1a2e36f28dc2c4d8e831d/hypothesis-6.165.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:66be9b848bdcb29132b18de6f574b89b392024c7de442acb393edaa1540cb548", size = 783398, upload-time = "2026-08-05T21:31:37.916Z" }, - { url = "https://files.pythonhosted.org/packages/02/8b/bf01b5f356f64a8af28be2718674e23ff7c0a4dbc5f476295be624b1a5ad/hypothesis-6.165.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e51742efe8466cf89e26cb94843db8854bd0673a6d80da7e3d1ff6bd9dc006db", size = 774963, upload-time = "2026-08-05T21:31:10.053Z" }, - { url = "https://files.pythonhosted.org/packages/a8/4b/9ad06c5a5613b6d175a7fd1a518a9732bcc4772c0cb24f6d7e5fd63f7fed/hypothesis-6.165.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c648ee54cd734261e1615d80c2ebbd679d6dfbba6ef5aa672354c6ca62b6f446", size = 1105721, upload-time = "2026-08-05T21:32:29.252Z" }, - { url = "https://files.pythonhosted.org/packages/9c/f1/04c8ebe621c8b832106859f9425f91d049a96ccf99c3501f2905f3e1291a/hypothesis-6.165.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:032292cbffc0743b2fe4337a30c21ea9703a70a0021d39bf0c3e68bce7baab18", size = 1155349, upload-time = "2026-08-05T21:31:39.636Z" }, - { url = "https://files.pythonhosted.org/packages/6e/23/41fe5e805638dcb6a1b70c147e909d254d9f09db5ade7a3e790c94ec926e/hypothesis-6.165.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5dc64171b06472f0b6c2e54bdc25687987e560e15133cf52f55d9ef4c747ebe1", size = 1278502, upload-time = "2026-08-05T21:32:23.405Z" }, - { url = "https://files.pythonhosted.org/packages/ce/42/54fdfc954314980d2b0eabbe57ef2960d85f3b1acb95f3326ff931ed349f/hypothesis-6.165.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e5a823ba918641af8177c121f122964d471db2ab1d07c1fe229c77b3a5ab7e8", size = 1322379, upload-time = "2026-08-05T21:31:05.39Z" }, - { url = "https://files.pythonhosted.org/packages/68/db/3667633b31b2b423b320fec4b7ac154e74d6b4a122fadd8e06f9d9afbef1/hypothesis-6.165.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:70966ab7dbe0ea9644eaed8324e037f05ac6a8141646b977da9e77813dac6ed7", size = 614909, upload-time = "2026-08-05T21:31:41.464Z" }, - { url = "https://files.pythonhosted.org/packages/2c/d9/0168c0d6ea32c195225b0673ee8cb7b61de6841d62c87d614d26add35770/hypothesis-6.165.2-cp314-cp314-win_amd64.whl", hash = "sha256:a5b913acd896f4f80597dd38966cd13984a1cfd45512498e8cf054aa7c92ffb9", size = 670684, upload-time = "2026-08-05T21:32:37.076Z" }, - { url = "https://files.pythonhosted.org/packages/21/e4/61cea938488b6958f07b8249bf37fcfb2802367e2a6af65afe82b05ca18c/hypothesis-6.165.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:6fa46589088966083ce653f908560cdd3330274cfafaaa65d1fb29b5f6681644", size = 781982, upload-time = "2026-08-05T21:32:15.899Z" }, - { url = "https://files.pythonhosted.org/packages/15/73/b4f9ba3e4b988b567d887b0857962e841b227a81a02ea83ae520e2001c31/hypothesis-6.165.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6b5b922603879b4788447583928eb1cf2e1aafb9ce27f3a7234b7a4557d089a8", size = 773430, upload-time = "2026-08-05T21:31:28.913Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e2/6fb4a2edbc7775dfa4d3950e3537239c6d957eeb6c571275edfd79a2b981/hypothesis-6.165.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ab3a6b5bb3302f7dcd65b07dcdc0ca353c8c151567dffeda826977e765caaf9", size = 1104317, upload-time = "2026-08-05T21:30:57.765Z" }, - { url = "https://files.pythonhosted.org/packages/5d/06/9479b2acc58996ae18300becbaf910d7c542b5054a47ba05c28bdacd08f1/hypothesis-6.165.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09f1c626023b68968d2cc5fe1e31548109f4406d81a2c3017b3de9fdd3a02e7d", size = 1154232, upload-time = "2026-08-05T21:31:30.368Z" }, - { url = "https://files.pythonhosted.org/packages/e3/9b/5b5cfce5445a807d042ca5a1a470606613835842d99488a199d994584cae/hypothesis-6.165.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5c95808ab851498513192268e25f40bccd1c3719384c91535bbec3eedea39760", size = 1276739, upload-time = "2026-08-05T21:32:12.324Z" }, - { url = "https://files.pythonhosted.org/packages/2e/aa/6a731776ccaee13dba0624a73f01935fa174f39647ad463a495dcb2206a8/hypothesis-6.165.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4a1c8bec789f21dc10620ce99e15fcd4f7737b9b4cfa571cdd93e01a17b7b06b", size = 1321119, upload-time = "2026-08-05T21:31:21.113Z" }, - { url = "https://files.pythonhosted.org/packages/43/a1/5d2c7c1346a0089908a3974c9e40ce223c22dd291dfe5df69a8c8cc64b98/hypothesis-6.165.2-cp314-cp314t-win_amd64.whl", hash = "sha256:458c891dfc00133bc4ce2e6c9716838f80f2fd96caf306f5eef42ad02aa4d972", size = 670831, upload-time = "2026-08-05T21:31:17.998Z" }, - { url = "https://files.pythonhosted.org/packages/35/51/745695dde335122e447dd8f762ee1c2f44d5f73dd3268a6df7f5d4339c1d/hypothesis-6.165.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:210955bdb78ce0e3279fd7ed47fa77fdd9f6004368a099c933c456f4c6ea358f", size = 783104, upload-time = "2026-08-05T21:31:02.564Z" }, - { url = "https://files.pythonhosted.org/packages/a4/8a/26f37eb2265ec3d6d59fcac7d93d554231ad2db7cef4e9e78113fa39ac36/hypothesis-6.165.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:024ddf78d150825210b4e41169ad1f3d40f8819bee0304890b851904d9f97541", size = 778956, upload-time = "2026-08-05T21:31:08.731Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ef/72d586c2c5094410569a4e62792f0069dc0b6113979dd7754e80d03a800b/hypothesis-6.165.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49e9aa39b21589e64b25ed8a452014fb546f2533cd5dbc0be16c35f94a4a9f6c", size = 1107828, upload-time = "2026-08-05T21:31:19.457Z" }, - { url = "https://files.pythonhosted.org/packages/38/1b/292fd8f7552d624ed29abab7f17935009cae3de14e50991951a73cff2a25/hypothesis-6.165.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:568038c8c9de3b22087fe1536d8d512e23c5a5eca3aa4c692e8fa46592afa267", size = 1157603, upload-time = "2026-08-05T21:32:39.041Z" }, - { url = "https://files.pythonhosted.org/packages/47/10/005eff3fba214bac14df88fdb78502d4990a135b128837dc826b215aa746/hypothesis-6.165.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5a9c92f193b14bb0e69038cb4b31f3c0c7497cd3f2f43be5bb8a8fb6a036362d", size = 674478, upload-time = "2026-08-05T21:32:41.203Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c7/da4298709de649a01c73ce93c4ee5b5651433217e17c494dc91baded86b0/hypothesis-6.165.9-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:01a2a4106fa90396ffa1c402968a8bd25af056e1fdf7974a369c4057cc4035f1", size = 783107, upload-time = "2026-08-15T02:49:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/58/97/d9f265174464d05a4d818d98dcd4e870ad2c2b60e9844033c23b3eb21eda/hypothesis-6.165.9-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:7812f1ce9e2bfc0958f2a96975f16a1e1f2400f9d8b7043723ff5811647fe0f6", size = 778677, upload-time = "2026-08-15T02:48:16.984Z" }, + { url = "https://files.pythonhosted.org/packages/ae/fe/d3364f99dd76f1275b430b2fc2818119df84e71f0657ad7f89b922e90d55/hypothesis-6.165.9-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e547823eca16eab9c9569f292e5ce8f95d8a59f1d67e504e97a841d17eb0b562", size = 1107895, upload-time = "2026-08-15T02:47:42.954Z" }, + { url = "https://files.pythonhosted.org/packages/71/5d/42b524303219351add359a05463df067be126c9ca576b0547218cf8aa54b/hypothesis-6.165.9-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e50c687c0e86bb5898f36b845ab84439f44ef2369f0f9bbcbf6aa6860640324d", size = 1136455, upload-time = "2026-08-15T02:49:56.069Z" }, + { url = "https://files.pythonhosted.org/packages/af/d5/91948f68b5007a40b2cc356d9365de0f5cb08c200e383b38259ad75130ae/hypothesis-6.165.9-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f82aed6f0631b5073bd87789aa56609e7c83bd762384fd24c84f3435816b2569", size = 1135112, upload-time = "2026-08-15T02:48:51.211Z" }, + { url = "https://files.pythonhosted.org/packages/55/3c/6e04ac80c5a63da9ecff00199851433437f617da9cd1b5208f11af3337b8/hypothesis-6.165.9-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488b88b6856e430763a7ac5a5fc6e20a798766dca8df8f94576de2281ee60afd", size = 1157397, upload-time = "2026-08-15T02:49:27.874Z" }, + { url = "https://files.pythonhosted.org/packages/12/40/4a1aeddc254d8d6a8f43bbeea928ef5b576e041eb0c9459a91fce884f4cd/hypothesis-6.165.9-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:22dd9e264f2a79a1b63a5fae13351e3cf0088f584fb05bb0e23f21b8fba9df7c", size = 1112748, upload-time = "2026-08-15T02:48:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/35/bc/dde8231f1d10ab0c60627c40757b9b832605028ef0780420fcfc3ccba664/hypothesis-6.165.9-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:431ab894062666c93157234f9949a4db3549fbf3c220777abae3482e1b43f9bc", size = 1149484, upload-time = "2026-08-15T02:48:53.088Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b2/cb29a983c7f06770a6b348f76cb33f90a70b0594a1e61c4a803aedfb1d1f/hypothesis-6.165.9-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c7edcc3dcec42b413c900f8e660f3741c21f0055c53bde21d728f72a21713247", size = 1283267, upload-time = "2026-08-15T02:47:55.688Z" }, + { url = "https://files.pythonhosted.org/packages/88/c0/6bfcf3fb379aba87f3d09073b589c7b7e932ad047ed7bc0124300a51f431/hypothesis-6.165.9-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0d4a613404ba216b6f698ce9df6f9c22d04d8e8a571c42525e83905cf1b1cd10", size = 1409841, upload-time = "2026-08-15T02:48:23.939Z" }, + { url = "https://files.pythonhosted.org/packages/98/c0/0c73a985144717272d2afec377a45c149688a477af946ae24c16b59c9788/hypothesis-6.165.9-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:658c33d8a54cead6081f73fcc4e2c0fbe43d251d3f4170630748d8e5dcbd542f", size = 1264878, upload-time = "2026-08-15T02:50:05.056Z" }, + { url = "https://files.pythonhosted.org/packages/56/91/50d4a6df07a305f5f17578443c93b618a49e8db68c01de8870e539139eb9/hypothesis-6.165.9-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:7d3297fe95a253cea81b48e581e456105016ebe782d823dfb57ec09c3854301f", size = 1282519, upload-time = "2026-08-15T02:48:22.42Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5f/5a54f4ca30a669a247df7c467aea7bb854d9b4de7e1dc51edb2f91ec603f/hypothesis-6.165.9-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a9a1e17200a2b369a38c19c2184efbc041a40b784675ba72ac6800d8c1260a54", size = 1324669, upload-time = "2026-08-15T02:48:29.144Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b2/6ef8eac3b30d6c8225fc405be3b99790701a942706ec93a3bd638a0b614a/hypothesis-6.165.9-cp310-abi3-win32.whl", hash = "sha256:5d40a92c6d025725da7827431a71b7657ce2e3923ff8f1773d6b15d717f77ea0", size = 668903, upload-time = "2026-08-15T02:48:34.033Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7f/b9514f65d2cc97277f6442e3b05164177a742489940efa53e794a54b62f6/hypothesis-6.165.9-cp310-abi3-win_amd64.whl", hash = "sha256:59f29ef2965caf1ae367ee0598ac0d293551b95744a5d49888737a3b3d9dd944", size = 675051, upload-time = "2026-08-15T02:47:49.697Z" }, + { url = "https://files.pythonhosted.org/packages/96/01/110123106a3702d2f3ffc47b9bc6a1a29c19cd2d15fcf356430777a4883a/hypothesis-6.165.9-cp310-abi3-win_arm64.whl", hash = "sha256:f52fd4f1b95f614c3fec53840d603f0a3b19e68eaa91c8d609222999c3ee5506", size = 673402, upload-time = "2026-08-15T02:47:48.364Z" }, + { url = "https://files.pythonhosted.org/packages/de/1d/1c2dea64a3b00b9f83ca57bacaf4982255da7797d6c65fc0301f21a0ca94/hypothesis-6.165.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a9995cbf91e0b22cb8d9aaaf6b10994b2ac1ebc3b01b4a91d3e82761a2737043", size = 783582, upload-time = "2026-08-15T02:49:29.881Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8c/d8f51166ef9feaeb08d33737b2874d1effca4ada8bf6598e6f2a634a3ca1/hypothesis-6.165.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f94e4456f7b0dd04f4f80f26a2c499370f6048f393ff61436de4e192e278e2df", size = 779367, upload-time = "2026-08-15T02:48:47.639Z" }, + { url = "https://files.pythonhosted.org/packages/8c/27/749aa12ec03fdcec1e4fa7289145f03ded46e892ae89613e333dd7090a7e/hypothesis-6.165.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bab95b4ef980cbc2b4632e49057588766cda930ffbd727d70edfea289da98dd3", size = 1108251, upload-time = "2026-08-15T02:47:59.983Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cb/a1c417cd39f87ca11e2b5b4a7da0446ae316b43b9baee23e113a59113759/hypothesis-6.165.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8795161db0a7c270c4f3e0eb754bd0ed96b17133d6f851c7444f9733c4c9d37f", size = 1157689, upload-time = "2026-08-15T02:49:40.288Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d5/82abf4d3b171f087d323b179a422472e1c1621ed47adf88f0fcdf7be7e35/hypothesis-6.165.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8c7dcb0a21dab077e65bcb082ee321b7e31402cf980da648b4369c71086ad53c", size = 1283592, upload-time = "2026-08-15T02:48:04.698Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ee/f156d374fd7f49220a789a231cd980ffe53bf1eec11a75306ded76265306/hypothesis-6.165.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:92b264bef7860bf37b5f22b5949083d26f113351da8b99e93adb0554d3e2c110", size = 1324910, upload-time = "2026-08-15T02:48:11.037Z" }, + { url = "https://files.pythonhosted.org/packages/23/57/d14969c84e59356103c0b5e1a27623a4149d5d371799757552b2ec344da2/hypothesis-6.165.9-cp311-cp311-win_amd64.whl", hash = "sha256:ae34d13d8438ea7f3ee18292290b9f608de6070309f3c5826644a88d197c319d", size = 674776, upload-time = "2026-08-15T02:48:59.008Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7e/03a84262b74300714af486e037403db8f5e19170f01bc32605300896b19d/hypothesis-6.165.9-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:41082aff3e438ac1eeb86002c269b9b5d86bab563e911614c2c8d34f405e02db", size = 784696, upload-time = "2026-08-15T02:48:09.314Z" }, + { url = "https://files.pythonhosted.org/packages/49/50/c77dca38074e38e99373ccd5f361bb5ad251e8f2de92dff4d7035111595d/hypothesis-6.165.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:67a0c0346ac0e8e106fad0224269a87c665768cc8d45de6e367e9b260ef36d5a", size = 776265, upload-time = "2026-08-15T02:49:36.232Z" }, + { url = "https://files.pythonhosted.org/packages/1b/80/9de2e558cd966ab1b1d014718c7aaab7d2d8f2d591ccadb27c1ffe639f6d/hypothesis-6.165.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10e826a708456c965e125f15dbf5a6f463e41dcf230a930b7a3e0ce7036f5176", size = 1106706, upload-time = "2026-08-15T02:49:12.466Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/dc89684de1a739347b22beadf3e53a64b8b7eef287dfa34b150432e13d4e/hypothesis-6.165.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fba20d9c60d6b8aec76f32cd22109e1b1f19a9985317255e2b43940d27996097", size = 1156753, upload-time = "2026-08-15T02:48:20.512Z" }, + { url = "https://files.pythonhosted.org/packages/b9/bc/495988f0fc788d33a24849e9ce7303a2c646f48baf3f5f010d9850bcc5a7/hypothesis-6.165.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6564fb9bbe1760719070a6fd37335fbf43696fe2911c386ba55fb3cbaaafb82a", size = 1280667, upload-time = "2026-08-15T02:48:30.763Z" }, + { url = "https://files.pythonhosted.org/packages/36/08/9758ba15cc7dcb408d2699081ee0acbd96842f9c5778c84fd145d19953ba/hypothesis-6.165.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:126e7bf873892649cc5f697c3b3a6548f077a56ae3601b22fddef2ea1ba6f64d", size = 1324018, upload-time = "2026-08-15T02:48:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/97/09/eaef6ee89a792ea82064cef66726023a5933f8704c0d3dbfc5a451829428/hypothesis-6.165.9-cp312-cp312-win_amd64.whl", hash = "sha256:c908edd69e4308c3cb194f0853aa7dfaa15e905242e81646b9f12881cd2683d3", size = 672207, upload-time = "2026-08-15T02:49:50.121Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1f/99069da612aee3a51519f96f4370c4ba42055b57eae92926353c63e68617/hypothesis-6.165.9-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:11bcdfb14b52d41384cc14ecd3f6d6152735bc2f7af0b9f3fa8685d630a1e98d", size = 784591, upload-time = "2026-08-15T02:48:42.765Z" }, + { url = "https://files.pythonhosted.org/packages/94/10/cabf6a8782253e6b547ea8bdc6a48a9eeab415f1036c749b40ca59df32f8/hypothesis-6.165.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8c3d7d54802641c2f8b49f478f153567770d299d639ee38fc0a0fa3d95ee07cb", size = 776216, upload-time = "2026-08-15T02:49:34.179Z" }, + { url = "https://files.pythonhosted.org/packages/15/5f/da966f98d69a8ee4b4fac509d92f7ab18cb4505b8191acc23bbb52f8bb17/hypothesis-6.165.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8f5b63a8fba74d19a12ad4abaa95afb7466d66d1adc90ba3026e40b7924452c", size = 1106621, upload-time = "2026-08-15T02:49:08.529Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5d/1fffd1031b4281e53fff02ad7adeda31db8218e91b82e7a98c7957541e76/hypothesis-6.165.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c01b77160328d8fce22bb5121fe1f5fee36b434a1220603500605b88ab854d7a", size = 1156613, upload-time = "2026-08-15T02:47:45.457Z" }, + { url = "https://files.pythonhosted.org/packages/b1/30/8e93a02016689c0b5cf0c35f8f9199b745601b3db9723296ca6d7366dcd5/hypothesis-6.165.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:88bde76aaab7f8abcd9412d135eaa49e8f7d89d068188fae2768a7803c80df89", size = 1280650, upload-time = "2026-08-15T02:49:44.026Z" }, + { url = "https://files.pythonhosted.org/packages/c0/48/2b0987389f141bd7bccc0785af4af86de9dfe9a6bc1b2b5dc612b083ae8a/hypothesis-6.165.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0df8f8736d176d3a5092890b6fb9030f9c726d272b52af0e1ce887d62fc21699", size = 1323749, upload-time = "2026-08-15T02:48:12.519Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a2/0a2380fdedfe0e16a09a09401b86bdc1eb049b3b6256fd837424d652c88e/hypothesis-6.165.9-cp313-cp313-win_amd64.whl", hash = "sha256:7fc9f46d88732fc74539ca7748aedc736c5e111431b06bd8ef2534440379aff2", size = 672195, upload-time = "2026-08-15T02:49:02.806Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d0/95075d5ae3263e1c9143a3e89d9f19b864b1da303fb426964d9f7c08ce69/hypothesis-6.165.9-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c3432a48e5895cba6b36c3e5abb915163d5169dc755685b4cd4720b710d77e3c", size = 784690, upload-time = "2026-08-15T02:49:23.922Z" }, + { url = "https://files.pythonhosted.org/packages/ed/d9/4a8b58237a36a839abefac22905ce9172b4c5195344708690413c6ef900d/hypothesis-6.165.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:70fe20939f63eb661227fd2b05193eba1e5ea6e547a2fbe158839f2acc519f4f", size = 776366, upload-time = "2026-08-15T02:48:27.261Z" }, + { url = "https://files.pythonhosted.org/packages/74/8d/91d1261f09a87299dc0ba89e12d8f9bbfd454e8922560e332b190a5c0a9f/hypothesis-6.165.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20b177ef8aae2a2817a2223d103353d120118e686a152bcd01832a27ddc9388e", size = 1107128, upload-time = "2026-08-15T02:49:10.617Z" }, + { url = "https://files.pythonhosted.org/packages/2b/01/0198547c8b73d73ee5b885d8f73931c4dd3c62055dc93a80b5e18590efeb/hypothesis-6.165.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d755b56098801a28a419172e57beb536b8c32929e7a37bb5ef12f2c00abd284", size = 1156757, upload-time = "2026-08-15T02:48:25.44Z" }, + { url = "https://files.pythonhosted.org/packages/21/3c/a3bde8c9630d361d85570ed5a6248fec0360a3874e26fdb04d62c8ebabf9/hypothesis-6.165.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d49496271282bf424ddd728108f49b92e1d32066a3d0ba18990d5b01c40c3175", size = 1281013, upload-time = "2026-08-15T02:48:44.399Z" }, + { url = "https://files.pythonhosted.org/packages/f7/2d/017895202e6f2d75928b8e66bb8efe28c5c834771f6a3d118fc936d811d0/hypothesis-6.165.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:be54673f76785ff954b511745d4a7a18c7d7fe1d893cb81a8917cd28878fbd55", size = 1324110, upload-time = "2026-08-15T02:49:18.05Z" }, + { url = "https://files.pythonhosted.org/packages/40/f8/02f9be17e21b55513da624bac18a21c29011351d4e5d089ca5095555d1b4/hypothesis-6.165.9-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:af42ad4c24b69d4482e4813e148f458fc090911c0cf9055225c62fbd1f0b102e", size = 616257, upload-time = "2026-08-15T02:49:00.997Z" }, + { url = "https://files.pythonhosted.org/packages/e9/be/c94631e799bb4ee87c8c80de72f2eb44e93e7d0bff45b22b8801c78e5895/hypothesis-6.165.9-cp314-cp314-win_amd64.whl", hash = "sha256:9bc2d40dd2a71ba3971b902d31c8b1b0c40d3d465591fd71bb8ce6f541add530", size = 672006, upload-time = "2026-08-15T02:48:32.418Z" }, + { url = "https://files.pythonhosted.org/packages/43/15/d23548a12e5670513903b73a42fbd4f5ab7b1287bc6fd5a4a890c5746a62/hypothesis-6.165.9-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fb16358e29f3090a9ab529e84df52923ddcd8505785c019bf48cdcd45e094a", size = 783152, upload-time = "2026-08-15T02:50:02.576Z" }, + { url = "https://files.pythonhosted.org/packages/0d/cd/37b5e5c042c557aa50db8c7f554b655b51c83e0b50306fb6ec8b05b03bfe/hypothesis-6.165.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ed61ad45184997f97c3baf255f34aaaf4edff63f1a61aad87919034b4ab9f64e", size = 774782, upload-time = "2026-08-15T02:49:47.988Z" }, + { url = "https://files.pythonhosted.org/packages/56/ed/43b6db3681937ffa191148533d37cdd3c091f707ecb92281424078876d45/hypothesis-6.165.9-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f96f36c3d3677f22ff990d7aab4235576a8429e72dab5382c45bf489630972f5", size = 1105367, upload-time = "2026-08-15T02:49:42.041Z" }, + { url = "https://files.pythonhosted.org/packages/0d/dd/eaab3c678a23ab434c65b4ea5de5b302267ce68aa47b996524d2f34dcd13/hypothesis-6.165.9-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69ab6500e47d7c21310360d1ee5cb6441660a10408e856d3738f07536732ed8", size = 1155499, upload-time = "2026-08-15T02:48:49.363Z" }, + { url = "https://files.pythonhosted.org/packages/10/fa/27049cbfb3e66cc1b68b32cbe137ac69a91cf09ac8520c4eb8ae9b3e5cbe/hypothesis-6.165.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cc98fa897f48127fdf2e7d1b2428f839656cd52a6344095e501371d222b9c19d", size = 1279038, upload-time = "2026-08-15T02:49:26.001Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d4/ab591a445dfeb7fa59b3c12de3eb9ff09901b4ede4025061b2fe1bc302af/hypothesis-6.165.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a8b2d7415b1f7ceec6e689c992b919af41e339eafb35d6367a3abbb5b6db9753", size = 1322697, upload-time = "2026-08-15T02:48:39.467Z" }, + { url = "https://files.pythonhosted.org/packages/40/fd/4ee3b14d2d73a1d9442ac161b953dea2522fdac8ef7ca265fe5dd205982d/hypothesis-6.165.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1000afaf05be59c88c13b458fd74d123e3361ca3d14fe127814a1231cb9cb7df", size = 672011, upload-time = "2026-08-15T02:48:07.583Z" }, + { url = "https://files.pythonhosted.org/packages/81/a1/20b63e3d17e05e82ef47b93ea3fa9a823297249ed7953e7e38f1b57a1e91/hypothesis-6.165.9-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:5f6b4ae650480368e2477e8f875cdbfe8533a7b2e09f049fe206c48b20174622", size = 782729, upload-time = "2026-08-15T02:49:52.008Z" }, + { url = "https://files.pythonhosted.org/packages/7f/13/b43cea9578820a3e41cd70e8395068a0f07ef11c393ab09ff5aeb5d44d7f/hypothesis-6.165.9-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:1aaba05b943345d2a448b8c1c795bb191adf842b450bfc7207bdd1bf139a9b58", size = 774415, upload-time = "2026-08-15T02:47:57.162Z" }, + { url = "https://files.pythonhosted.org/packages/ea/08/222a8f2aeb8429b54cd53624add076ae5c9b39b0b170afd2435d0e81a8a9/hypothesis-6.165.9-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:306c1efc0f01f3d5c29c45996e055437971337aa24b6c6735b0514cfb5df60ce", size = 1104761, upload-time = "2026-08-15T02:47:46.841Z" }, + { url = "https://files.pythonhosted.org/packages/68/6b/09943bad5c1c5ea689a1be6376bcb7c68c34f3e3db5b792a1c0bb4edd79c/hypothesis-6.165.9-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1551df11534d192616dc1b4539b6e526c4704d08b22d2ffb6ea3479ce6b87d94", size = 1133212, upload-time = "2026-08-15T02:47:52.814Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ee/8fa90ca7a7d8b2362f3aaa0b2117405abf7b104fa01b52d5fecc4bb3c3fc/hypothesis-6.165.9-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afd99a069df90c73c15d53e275d11a782fd5f17f5423002868ce9d2438ab567f", size = 1132156, upload-time = "2026-08-15T02:47:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c3/6dae83eccd7260f5cf3468f145c663ce9b22579a9d9c80e9e7be49bec8b2/hypothesis-6.165.9-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c3e8c45ca5e2e4f519021836e5d86fb2229915c3f92fb50db7bb7364dfb2c1", size = 1155040, upload-time = "2026-08-15T02:49:16.085Z" }, + { url = "https://files.pythonhosted.org/packages/50/90/25eaabe2fecb3af3bcb9b4c3261ba9ffe0e092a3199232adce15399c040b/hypothesis-6.165.9-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:676a3bd04261b7bdd35374a3f4dabd82025fcead110a067a76328799f394a4d0", size = 1109751, upload-time = "2026-08-15T02:50:00.293Z" }, + { url = "https://files.pythonhosted.org/packages/1f/81/e8117e7ed3ff1ef7a36f023469efc925458b52c1243c539566a5308f5d77/hypothesis-6.165.9-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7f51c2297d116c2c025643762269eed0767589b6fc858d8de69d9f80cf33a23", size = 1144793, upload-time = "2026-08-15T02:49:38.317Z" }, + { url = "https://files.pythonhosted.org/packages/c6/6a/da2ea69d1fed780d86895f1ef7f2811240ed810487536ad22298629cc630/hypothesis-6.165.9-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:e500b97f899e375e47b407a48f3450cb1dd44bca757529c0490cb4533d878b30", size = 1278531, upload-time = "2026-08-15T02:48:37.581Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6e/b3b21f15fe9debb9c448b03aaeb725ee007ba68518e55a6810878d7b9765/hypothesis-6.165.9-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:8401f55aba2d51f20b02c42e9e08b7eff2018639bdb1af16a4c279ba7e129c81", size = 1407073, upload-time = "2026-08-15T02:48:41.256Z" }, + { url = "https://files.pythonhosted.org/packages/da/fe/55f8e9c34f2b0a5ccf895a527a499f8562e9a14f014dd018b2535e8aa1c4/hypothesis-6.165.9-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:34a4591db1dcf6aa544b4f884520ff0f1bc02ac3d11c76f78c73535b6b672272", size = 1261243, upload-time = "2026-08-15T02:49:58.076Z" }, + { url = "https://files.pythonhosted.org/packages/0b/37/49fb2374bff4e66d3d8264d083ddbb83269ec13903b441ebbc0d3c51502a/hypothesis-6.165.9-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:0bd589a011b3d0c4b98d78f467e5a5a6b96bf5be5d15b39ef47e65c225e8d790", size = 1279060, upload-time = "2026-08-15T02:48:54.843Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b7/c826e67f5d553d20644ac3da5d25de7ff9d50a1f41467981b00da7b2ab9b/hypothesis-6.165.9-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:84a93a152ffaf92fa6b3f94d366f4c866e26f3f1b643fcc6da4ee2ddfa038aa9", size = 1322247, upload-time = "2026-08-15T02:47:54.398Z" }, + { url = "https://files.pythonhosted.org/packages/24/ec/e70e440805820cfd662d04d05aa34b5d29e46d5a49ee871d6937721b2c17/hypothesis-6.165.9-cp315-abi3.abi3t-win32.whl", hash = "sha256:4a2081699e0b16b35c7b34cf0ff01997e8fc6038b520b9f29c106764158c411a", size = 665906, upload-time = "2026-08-15T02:49:53.901Z" }, + { url = "https://files.pythonhosted.org/packages/23/79/968b149b572e1d4284b04280cc181449090c8bd03e55b24d57b64443978e/hypothesis-6.165.9-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:e1d9492193b52087df4d264edd77618869d447b5c7f6a022c7bb11b88d577cb8", size = 671817, upload-time = "2026-08-15T02:49:46.053Z" }, + { url = "https://files.pythonhosted.org/packages/c2/72/0f951d4f0e07720583e4773638cd556d2b6585605f8a3df54d2fecf2e2ff/hypothesis-6.165.9-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:f67414675f2263948e5a5977b636b5cb1cc5f33b82a9be34f8a6b9a8293d4707", size = 669795, upload-time = "2026-08-15T02:48:03.176Z" }, + { url = "https://files.pythonhosted.org/packages/43/7d/1b6b48629032b22960a5b010a791467670c08d9413a3c6c15a72f7c1da09/hypothesis-6.165.9-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f6caa4194f37a02232ecb964600d17facc23a6aa5b0295008989f31e329eb837", size = 784507, upload-time = "2026-08-15T02:48:45.961Z" }, + { url = "https://files.pythonhosted.org/packages/95/a6/c29939a55cde73886cee7a3975148e132a9ed6881d39b7c9b8c19e3a854b/hypothesis-6.165.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0a4a32dc8e8728417aa13055903dbf742dd0b5cedda7c2031b4b2a0bcc1d463d", size = 780353, upload-time = "2026-08-15T02:49:14.307Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f9/ac8cd423b1c7506dae01c4a9295c434b53525401a182a116aecaa27332cf/hypothesis-6.165.9-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f255b389364f71e6e9305ba99bf13733354cb0dc4a97d07db8aef76278a345e", size = 1109224, upload-time = "2026-08-15T02:48:15.557Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/64c4dfa2836ec421e35a167a3fba92018950a8c7551a268a5fc799329455/hypothesis-6.165.9-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7f0507d8224e5b44c2651d7ef8df15ac6d38eb56c98e7f451c3090804ce109", size = 1158929, upload-time = "2026-08-15T02:49:21.922Z" }, + { url = "https://files.pythonhosted.org/packages/88/8e/fccbea8a0a03532762fdd56d3809fd8398f46af1ec9018f694d4ec5c22cf/hypothesis-6.165.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fd9e242fabb05715ec2d3b9b96dcddf683fc3891a04b6685b79e400e35ccbe18", size = 675885, upload-time = "2026-08-15T02:47:50.986Z" }, ] [[package]] @@ -817,8 +836,8 @@ requires-dist = [ { name = "aiohttp", specifier = ">=3.14.3" }, { name = "diff-cover", marker = "extra == 'test'", specifier = ">=10.5.0" }, { name = "grpcio", specifier = ">=1.83.0" }, - { name = "hypothesis", marker = "extra == 'test'", specifier = ">=6.165.2" }, - { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.6.1" }, + { name = "hypothesis", marker = "extra == 'test'", specifier = ">=6.165.9" }, + { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.6.2" }, { name = "protobuf", specifier = ">=6.31.1,<7" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=9.1.1" }, { name = "pytest-aiohttp", marker = "extra == 'test'", specifier = ">=1.0" }, @@ -826,7 +845,7 @@ requires-dist = [ { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=5" }, { name = "pytest-mock", marker = "extra == 'test'", specifier = ">=3.14" }, { name = "requests", extras = ["socks"], specifier = ">=2.34.2" }, - { name = "ruff", marker = "extra == 'dev'", specifier = "==0.16.2" }, + { name = "ruff", marker = "extra == 'dev'", specifier = "==0.16.3" }, ] provides-extras = ["test", "dev"] @@ -985,7 +1004,7 @@ wheels = [ [[package]] name = "pre-commit" -version = "4.6.1" +version = "4.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, @@ -994,9 +1013,9 @@ dependencies = [ { name = "pyyaml" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/3a/ddb78f32a0814e66b18a099377a106a2dcdce92d86a034d69d65df9b256e/pre_commit-4.6.1.tar.gz", hash = "sha256:03e809865c7d178b9979d06c761fcbfe6808fdaded8581a745bb110e52050421", size = 198646, upload-time = "2026-07-21T20:56:58.225Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/89/1f3e8e1fc3e97de0fa963495832f581f025f29471602a309e48808244292/pre_commit-4.6.2.tar.gz", hash = "sha256:8f5d7bfb021ecdbcd9d49d89847082dd24172ccde534390081a679ad046e2441", size = 198670, upload-time = "2026-08-10T22:07:18.421Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/49/bc925106abcdac498074f2cbe6137e94e09f418dd2b7775df5b577dc0313/pre_commit-4.6.1-py2.py3-none-any.whl", hash = "sha256:0e3b2942510d1fb34eec167a3ec57331bf8442122f1153a9fb8b58f5c49b2717", size = 226186, upload-time = "2026-07-21T20:56:57.064Z" }, + { url = "https://files.pythonhosted.org/packages/45/e2/bbb7129c9e7999a6b8ee9cca3b66486c25c423ab5a75f34071798b74ce94/pre_commit-4.6.2-py2.py3-none-any.whl", hash = "sha256:e2dde9a75d3bce11bd3831c26d134df00a2803c1d818be6a0383c3dcda25dc4e", size = 226202, upload-time = "2026-08-10T22:07:16.942Z" }, ] [[package]] @@ -1302,27 +1321,27 @@ socks = [ [[package]] name = "ruff" -version = "0.16.2" +version = "0.16.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/e1/4508a569211b35599016e84ba65c1a992b7a4004b4b6c4bea02a851cba1b/ruff-0.16.2.tar.gz", hash = "sha256:c3d7828d12e8927a6fc65fe38e2c2541b9e762d360a1786d752cb1b8883b3c9c", size = 4885811, upload-time = "2026-08-07T13:31:01.432Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/b3/3213589383f8f1b3938781bd1278713f6d18621a14992b3e81fefb8a5ef9/ruff-0.16.3.tar.gz", hash = "sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2", size = 4891904, upload-time = "2026-08-13T15:17:13.381Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/57/db19951540f98859c956b50bdb4d31089b4d91e9f15e2968e7d5193806d5/ruff-0.16.2-py3-none-linux_armv6l.whl", hash = "sha256:3c8de4cf2181f01d57946d87d777aa52916976fc09942aed89938fab5e013318", size = 10847925, upload-time = "2026-08-07T13:30:14.468Z" }, - { url = "https://files.pythonhosted.org/packages/13/5a/995fe85a8470d3e391ac0f7fa8054bb454eaf33ee138196d6172ed1079c0/ruff-0.16.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a48cc05c6fbc811ca81b5d7ba95375affea6582d1b8024e455e41afbbf55344", size = 11072662, upload-time = "2026-08-07T13:30:18.143Z" }, - { url = "https://files.pythonhosted.org/packages/32/53/370d767c61c71a971a4ace36703a7ecd8c393956349a7325d7fab2b56827/ruff-0.16.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a2c0d14fcbb26c91f0f867a6dc9bd71bbc30b1b6151829c884f23faeab2e5700", size = 10566771, upload-time = "2026-08-07T13:30:20.899Z" }, - { url = "https://files.pythonhosted.org/packages/85/d6/9d96948caf5a632be62d62202d5ec914d6856f204fd79eb036e5915e79ea/ruff-0.16.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:335c621622c4650330be50842561c6586ac6971bb8ab5407fe34dcc9efb16bbe", size = 10975825, upload-time = "2026-08-07T13:30:23.517Z" }, - { url = "https://files.pythonhosted.org/packages/3b/92/ea87129b3414acb0b5770563779c51804d37ac67675c7ba35447ddb14773/ruff-0.16.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e66910f2c37cc753f9ef6580c914a621b80c4fa3549d3e3521e29d0f5bfc3f", size = 10649437, upload-time = "2026-08-07T13:30:26.097Z" }, - { url = "https://files.pythonhosted.org/packages/ac/43/f8f291dcd4af5bb7872b74fdfa41a7cd7c856ca1d4069670971cf1b9f5cb/ruff-0.16.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7e36fbfba65510548156902bcf1350a979a958ce0347ce0f90d73894036b39f", size = 11446761, upload-time = "2026-08-07T13:30:28.752Z" }, - { url = "https://files.pythonhosted.org/packages/71/4a/ef991fb2fcf516ab71f0808adcdd8da5e18c8cde447f4ceaf5f47a5132a5/ruff-0.16.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f0eab35f80df8f134aae5d1630e751901321d317cc8e50dc39e36fa3ed34cd12", size = 12336364, upload-time = "2026-08-07T13:30:31.468Z" }, - { url = "https://files.pythonhosted.org/packages/f3/24/f615e74f307e6ca0e56a482872477b856c70d530aa356abfb6dfe5ca8a80/ruff-0.16.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ea8c0594feb894e89c8c61ab9c103d38b0ea72dfde6c594107147ca31b1140", size = 11630720, upload-time = "2026-08-07T13:30:34.426Z" }, - { url = "https://files.pythonhosted.org/packages/c5/d3/8ef50149e8412a77f7ab409efdef0e2b23803707a3863da4fc64cb23d459/ruff-0.16.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab3d62dde0b19facdd632008cc4827fc28ada7736c6bd35ab6f1050f0bfed53f", size = 11466130, upload-time = "2026-08-07T13:30:36.958Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a7/a19334985c4dea8c381981fa252cd854c7ee52dc4b1686dc16f4a911c702/ruff-0.16.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e43e1f5b8388da9eca1b9e88328d47a5cec794633ccf6f7484ac2dd15eee92c0", size = 11523634, upload-time = "2026-08-07T13:30:39.822Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6c/96d192b0e742412ceda08c0a50f9669b253dde9fd6a60ea1a10c9fa79a63/ruff-0.16.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c24788a980581e1d7ea3a0cbe4344c4fbeb0a6a9b1f4713aa46bb104f8294690", size = 10949807, upload-time = "2026-08-07T13:30:42.745Z" }, - { url = "https://files.pythonhosted.org/packages/fa/51/e26599ceca11e79ee255c7df515995561edf87e9ca1893284e44d98f5a86/ruff-0.16.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:81806b08329130005dd4a8a8394a0c9da8c6f4cafb16ba438d2a2ee6a18bedf1", size = 10646891, upload-time = "2026-08-07T13:30:45.522Z" }, - { url = "https://files.pythonhosted.org/packages/68/01/800c4b1f97bc8d7c6029e06b1f20473a3cf1e13c4933d8f3342add83fc55/ruff-0.16.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4ce4e02bad779bef557f541a1b31f20d6abeae1cc05ed1b1ac019d4ffd1044c8", size = 11162063, upload-time = "2026-08-07T13:30:48.131Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d0/1477ea50fc5a0d4b0b71d1d63d50770bdd794d90b43e37a7618e63ec9894/ruff-0.16.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e0422abdf70070255fc4073ce9dfc814cc03db577013761ddd09bc1e4a9a4fbd", size = 11556038, upload-time = "2026-08-07T13:30:50.686Z" }, - { url = "https://files.pythonhosted.org/packages/b8/76/a7776f32048d991e16d4fa8ff91790b877342d3596cc3ed04acdbf1aaedc/ruff-0.16.2-py3-none-win32.whl", hash = "sha256:bf3a63d78fb39f4bf5ac8ae52051c5520505301abe19ba4e204c453b3f09bb0b", size = 10872850, upload-time = "2026-08-07T13:30:53.471Z" }, - { url = "https://files.pythonhosted.org/packages/00/0d/929c800d920e61397d82a01b60bffc68da3052c17d31de59efaad2e4ed75/ruff-0.16.2-py3-none-win_amd64.whl", hash = "sha256:bcabe2f6d0fc7819f1431793005af4e4de7371927d037345bf941252b195b9fa", size = 12023338, upload-time = "2026-08-07T13:30:56.193Z" }, - { url = "https://files.pythonhosted.org/packages/5b/6c/93e26c22c5f78ff87363e07da49c84955affbeb1098bd1936bf3b3f293bf/ruff-0.16.2-py3-none-win_arm64.whl", hash = "sha256:d614e95cedf38a2053fd351c55b103ba30d017d61688fdbfd40ee0412852a99f", size = 11374065, upload-time = "2026-08-07T13:30:58.775Z" }, + { url = "https://files.pythonhosted.org/packages/bf/96/493770daebd68c0a67f1549fdf519f53be51fc435186c0585bcc272fd76c/ruff-0.16.3-py3-none-linux_armv6l.whl", hash = "sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7", size = 10902799, upload-time = "2026-08-13T15:16:27.382Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/2becf3942fddc29a29b8df47691d456fb1085391a694f74d84513251418c/ruff-0.16.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fe155130631a2471fd2e14a7a664a4dfbd7194b8229c3d7b2a40b21178639081", size = 11135539, upload-time = "2026-08-13T15:16:30.87Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1e/4b8b72f0d006dbf19326aa99f9ca0ee2ff374187c4d301cf529a51aa06fe/ruff-0.16.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ed719e14aa64d895c2ee922594a90a43c861a93f0575a95ff8c47cdbd13eb9", size = 10475095, upload-time = "2026-08-13T15:16:33.259Z" }, + { url = "https://files.pythonhosted.org/packages/92/32/2201fa49ba1f6c101ee321e83f051ac7a4b8d07b0ef6b4d3f2772b302275/ruff-0.16.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84", size = 10668771, upload-time = "2026-08-13T15:16:35.65Z" }, + { url = "https://files.pythonhosted.org/packages/c3/66/4afc5c8363bd04d45effce1b7c8713ca037d7a6740b7451a2403a6e3a972/ruff-0.16.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870", size = 10699568, upload-time = "2026-08-13T15:16:38.195Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/c67d246bf36bf1698551c56de39e95cd07f70e64433e0098e6267d77061b/ruff-0.16.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b", size = 11499365, upload-time = "2026-08-13T15:16:40.623Z" }, + { url = "https://files.pythonhosted.org/packages/67/0b/00ecbceb99a263af7b12f6f05ac3c92bc47b905e91adc3f207a836e3bc01/ruff-0.16.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413", size = 12311728, upload-time = "2026-08-13T15:16:43.564Z" }, + { url = "https://files.pythonhosted.org/packages/54/b2/b7b3bb54f4d3f7db504e476ad4ab8de530dceebe2c061384b2757ee419e8/ruff-0.16.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82", size = 11699896, upload-time = "2026-08-13T15:16:46.209Z" }, + { url = "https://files.pythonhosted.org/packages/c7/30/4c468429ac195addc5ee1b717b6ab1b66632786737ca3b2ed3443fb0c26a/ruff-0.16.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb", size = 11058736, upload-time = "2026-08-13T15:16:48.823Z" }, + { url = "https://files.pythonhosted.org/packages/43/67/7a113cdaddf24b64d7f75b1242a99d04c82fcef4f6921fdbb832beaffb5f/ruff-0.16.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474", size = 11586911, upload-time = "2026-08-13T15:16:51.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c1/2e66f24c0f3ead25a5e660111778685e505e5da353c82802bf49f0cbe7b9/ruff-0.16.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da", size = 10954265, upload-time = "2026-08-13T15:16:54.763Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ba/4cee23bf52cba9a058d3726de623624daf50ef9638868edd86f4126157f6/ruff-0.16.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50", size = 10709886, upload-time = "2026-08-13T15:16:57.339Z" }, + { url = "https://files.pythonhosted.org/packages/82/df/7da7194fa5d9dc0a285f7e6fa5a4722e7c63faac0b45b614ded9314363a1/ruff-0.16.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506", size = 11210392, upload-time = "2026-08-13T15:17:00.171Z" }, + { url = "https://files.pythonhosted.org/packages/35/85/7795f6e817af050e7517bf3e7aa9b061cce70ef33d280aad902c956c1ecf/ruff-0.16.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d", size = 11626910, upload-time = "2026-08-13T15:17:03.299Z" }, + { url = "https://files.pythonhosted.org/packages/78/9b/475b927cf27a5cbbda3c7bafb69ed6ff77e1d7923d5d85f17c2749d7ae32/ruff-0.16.3-py3-none-win32.whl", hash = "sha256:388cdf2166642bd9b13d52b5932d3170f34f8abed7e8d9a855f1d84b83645a0a", size = 10931415, upload-time = "2026-08-13T15:17:05.726Z" }, + { url = "https://files.pythonhosted.org/packages/b2/99/e2a2bfc4fbf0a1e8a916bc9ebe6fe6c58cc34c28e0ffc6ce281d572d1c2e/ruff-0.16.3-py3-none-win_amd64.whl", hash = "sha256:e80a7d69ca2a6d1c4d352ec91458cdca6e56c83cdbcabd93e4abe1e53591d948", size = 11445993, upload-time = "2026-08-13T15:17:08.353Z" }, + { url = "https://files.pythonhosted.org/packages/69/3e/4132e539aed78c148854d4997a2685b0ed4dc4e87110b59ce528564e184e/ruff-0.16.3-py3-none-win_arm64.whl", hash = "sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a", size = 11399302, upload-time = "2026-08-13T15:17:10.908Z" }, ] [[package]] From f879de58319058e7b28763dfc7b805baf4cd7f5f Mon Sep 17 00:00:00 2001 From: Vijit Singh Date: Tue, 18 Aug 2026 15:19:45 -0500 Subject: [PATCH 03/47] fix: name the real first signed release, and escape the whole regex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three places claimed releases "up to v1.3.x" are unsigned. They are not: the first signed release is v1.18.1. cosign.pub was first committed in fe543ac (2026-08-14), the earliest tag containing it is v1.18.1, and v1.18.0's published assets carry no pithead.tar.gz.sig while v1.18.1's do. The claim appears in two operator-facing release-smoke messages, so a reader checking an unsigned v1.14 release was told signing had been on for eleven minor versions. The legend-label assertion escaped only parentheses before feeding a label into a RegExp, which CodeQL flags as incomplete sanitization. There is no untrusted input here — the labels are literals in the test — but the escape is wrong for any label carrying another metacharacter, and silently so: with the old escape a label of "Raffle wins +1" produces a pattern that matches "Raffle wins 1". Escaping the full set makes the assertion mean what it reads as. Frontend suite 317/0, identical for today's labels (only parens occur in them). Co-Authored-By: Claude Opus 5 --- build/dashboard/tests/frontend/components.test.mjs | 2 +- docs/dev/releasing.md | 12 +++++++++--- scripts/release-smoke.sh | 5 +++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/build/dashboard/tests/frontend/components.test.mjs b/build/dashboard/tests/frontend/components.test.mjs index 32651a21..8d9b0c27 100644 --- a/build/dashboard/tests/frontend/components.test.mjs +++ b/build/dashboard/tests/frontend/components.test.mjs @@ -113,7 +113,7 @@ test('chart range buttons include All, active on the default full-history view ( test('chart legend renders a toggle for every layer, including the marker datasets (#652)', () => { const html = renderApp(); for (const label of ['P2Pool (routed)', 'XvB (routed)', 'Shares', 'Events', 'Raffle wins']) { - assert.match(html, new RegExp(`legend-item[^>]*>.*?${label.replace(/[()]/g, '\\$&')}`), + assert.match(html, new RegExp(`legend-item[^>]*>.*?${label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`), `missing legend toggle: ${label}`); } // A hidden marker layer renders its button in the off state, like the line series do. diff --git a/docs/dev/releasing.md b/docs/dev/releasing.md index c25e1eed..4902776d 100644 --- a/docs/dev/releasing.md +++ b/docs/dev/releasing.md @@ -264,7 +264,9 @@ curl -fsSLO https://github.com/p2pool-starter-stack/pithead/releases/download/vX cosign verify-blob --key cosign.pub --signature pithead.tar.gz.sig --insecure-ignore-tlog=true pithead.tar.gz ``` -Releases up to v1.3.x are unsigned; verification gates every release from the first signed one. +Releases before **v1.18.1** are unsigned — that is the first cut whose bundle shipped a +`pithead.tar.gz.sig`, because it is the first tag containing the committed `cosign.pub`; +verification gates every release from it on. If an upgrade sent you here saying cosign is not installed, read the next section — that box needs the host binary once, not a manual verify. @@ -322,8 +324,12 @@ It runs two phases: `:vX.Y.Z` images and verifies them against the committed `cosign.pub`. A good signature must pass; a byte-changed bundle must be refused (this is what proves the check is real, not the pre-merge fake); the bundle's own `VERSION` must equal the tag (the #376 rollback guard); and an unrelated - key must be refused. If the release is **unsigned** — releases up to v1.3.x predate signing, and a - `--unsigned` cut ships without one — that is reported plainly and the phase is skipped. It never reports a signed pass for an unsigned release. This phase needs only + key must be refused. If the release is **unsigned** — every release before v1.18.1 predates the + committed key, and a deliberate `--unsigned` cut ships without a signature (an unconfigured box + aborts the cut instead; see + [Release / Validation Server › The release signing key](release-server.md#the-release-signing-key)) + — that is reported plainly and the phase is skipped. It never reports a signed pass for an + unsigned release. This phase needs only `gh` auth and network; run it anywhere. - **Real #59 upgrade** (`--upgrade DIR`). On a box still running the *previous* release, it enqueues the exact upgrade intent the dashboard writes into the #33 control spool, runs the host control diff --git a/scripts/release-smoke.sh b/scripts/release-smoke.sh index 858a8404..b8ef350e 100755 --- a/scripts/release-smoke.sh +++ b/scripts/release-smoke.sh @@ -15,7 +15,8 @@ # - the bundle's own VERSION equals the tag (the #376 rollback guard — an older signed bundle # served at the vX.Y.Z URL is caught); # - every published image verifies, and an unrelated key is refused. -# If the release is UNSIGNED (releases up to v1.3.x, or a --unsigned cut) that is reported plainly — never a false +# If the release is UNSIGNED (every release before v1.18.1, or a --unsigned cut) that is reported +# plainly — never a false # pass. Runnable from any box with `gh` auth + network; needs no deployed stack. # # Phase 2 (--upgrade DIR): drive the REAL #59 host path against a previous-release install — enqueue @@ -109,7 +110,7 @@ verify_release() { # running install already trusts), never the bundle's own copy — a key that arrives inside the # artifact it vouches for proves nothing. if [ ! -f "$pub" ] && [ "$SIG_PUBLISHED" -eq 0 ]; then - warn "UNSIGNED release: no committed cosign.pub and no pithead.tar.gz.sig asset. Releases up to v1.3.x predate signing, and a --unsigned cut ships without it (#960). Nothing to cosign-verify; bundle integrity rests on the digest-pinned compose + TLS to GitHub. (Not a failure.)" + warn "UNSIGNED release: no committed cosign.pub and no pithead.tar.gz.sig asset. Every release before v1.18.1 predates the committed key, and a --unsigned cut ships without a signature (#960). Nothing to cosign-verify; bundle integrity rests on the digest-pinned compose + TLS to GitHub. (Not a failure.)" return 0 fi # A half state is a misconfiguration, not a pass — surface it. From eaceb22ab4771aced3123d9a182e3111a63ba6c2 Mon Sep 17 00:00:00 2001 From: Vijit Singh Date: Tue, 18 Aug 2026 18:41:29 -0500 Subject: [PATCH 04/47] fix(release): resolve the upgraded install at assert time (#1068) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `release-smoke --upgrade DIR` asserted that DIR's own VERSION had changed. The #59 upgrade never rewrites the old install in place — that is what makes rollback possible: it extracts the new release into a fresh pithead-v and repoints `current`. So the assertion could only pass if the upgrade had overwritten the previous install, and a correct upgrade was reported as a failure on the documented final gate of a release. Every other assertion defect in this repo has been a false green. A false red on a release gate is arguably worse: the next person either learns to ignore a red gate or spends an afternoon proving the product was fine. `upgraded_install_dir` now resolves at assert time and handles both legitimate inputs — the `current` symlink, resolved after the upgrade moved it, and a versioned directory, which is unchanged by design and whose answer lives in the `current` beside it. The v1.19.2 cut escaped the false red only because it was handed the symlink. release-smoke gains the sourceable guard pithead-data-reset uses, so the path arithmetic is unit-testable rather than only exercised post-publish, where a mistake is already in the field. Three assertions, tier-1 1793/0. The mutation that goes red is removing the sibling lookup; removing the readlink leaves all three green, which is recorded in the test as measured rather than assumed — that call only normalises the path in the operator-facing messages. Co-Authored-By: Claude Opus 5 --- scripts/release-smoke.sh | 41 ++++++++++++++++++++++++++++++++++++---- tests/stack/run.sh | 41 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/scripts/release-smoke.sh b/scripts/release-smoke.sh index b8ef350e..82ca9960 100755 --- a/scripts/release-smoke.sh +++ b/scripts/release-smoke.sh @@ -284,10 +284,38 @@ smoke_upgrade() { [ "$status" = "upgraded" ] || die "Upgrade did not report success (status='${status:-none}' after ${waited}s): $(cat "$result" 2>/dev/null || echo '')" - local new - new="$(tr -d '[:space:]' <"$dir/VERSION")" - [ "v$new" = "$TAG" ] || die "Runner reported 'upgraded' but $dir/VERSION is '$new', not $TAG." - ok "Install at $dir upgraded cleanly $cur → $new via the real #59 control path." + local landed + landed="$(upgraded_install_dir "$dir")" + local new="" + [ -n "$landed" ] && new="$(tr -d '[:space:]' <"$landed/VERSION" 2>/dev/null || true)" + [ "v$new" = "$TAG" ] || + die "Runner reported 'upgraded' but no install at or beside $dir is $TAG (found '${new:-none}' at '${landed:-nothing}'). Check 'current ->' and the versioned dirs." + ok "Install at $landed upgraded cleanly $cur → $new via the real #59 control path." +} + +# Where the upgrade actually landed, resolved AT ASSERT TIME. +# +# The #59 path never rewrites the old install in place — that is what makes rollback possible. It +# extracts the new release into a fresh `pithead-v` directory and repoints `current` at it. So +# asserting on the directory this run was POINTED at could only pass if the upgrade overwrote the +# previous install, i.e. never: a correct upgrade reported as a failed one, on the documented final +# gate of a release (#1068). +# +# Two shapes to resolve, because both are legitimate inputs: +# `current` symlink — resolve it now, after the upgrade moved it (this is why the v1.19.2 cut +# did not hit the false red: it was handed the symlink); +# versioned dir — unchanged by design, so look for the `current` beside it. +upgraded_install_dir() { # -> the dir holding the post-upgrade install + local given="$1" resolved sibling + resolved="$(readlink -f "$given" 2>/dev/null || printf '%s' "$given")" + # `current` in the same parent is where a versioned dir's upgrade lands. + sibling="$(readlink -f "$(dirname "$resolved")/current" 2>/dev/null || true)" + if [ -n "$sibling" ] && [ -f "$sibling/VERSION" ] && + [ "$(tr -d '[:space:]' <"$sibling/VERSION" 2>/dev/null)" != "$(tr -d '[:space:]' <"$resolved/VERSION" 2>/dev/null)" ]; then + printf '%s' "$sibling" + return 0 + fi + printf '%s' "$resolved" } # Interactive gate before the destructive phase-2 upgrade (auto-yes when not a TTY, so a runbook @@ -301,6 +329,11 @@ confirm_upgrade() { # --- Main ---------------------------------------------------------------------------------------- +# Sourceable for the test suite (functions only), the same guard os/overlay/pithead-data-reset uses: +# the assert-time resolution below is the kind of path arithmetic that is cheap to unit-test and +# expensive to get wrong, and it only ever ran post-publish where a mistake is already in the field. +if [ "${BASH_SOURCE[0]}" != "${0}" ]; then return 0 2>/dev/null || true; fi + log "Post-publish release smoke test (#459) — $TAG" fetch_published_bundle verify_release diff --git a/tests/stack/run.sh b/tests/stack/run.sh index cbc1d4b5..8f09ffae 100755 --- a/tests/stack/run.sh +++ b/tests/stack/run.sh @@ -2344,6 +2344,47 @@ assert_rc "missing tool -> preflight fails fast (rc 1)" "$tc_rc" "1" assert_contains "the missing tool is named" "$tc_out" "shfmt" assert_contains "error points at the provisioning doc" "$tc_out" "release-server.md" +echo "== unit: release-smoke resolves the upgraded install at ASSERT time (#1068) ==" +# The #59 upgrade never rewrites the old install in place — it extracts a fresh pithead-v and +# repoints `current`, which is what makes rollback possible. So asserting on the directory the run +# was POINTED at could only pass if the upgrade had overwritten the previous install: a correct +# upgrade reported as a failure, on the documented final gate of a release. MUTATION PROOF: return +# the given path unresolved and both "lands on" assertions go red. +SMK="$SANDBOX/smoke1068" +mkdir -p "$SMK/pithead-v1.18.1" "$SMK/pithead-v1.19.0" +printf '1.18.1\n' >"$SMK/pithead-v1.18.1/VERSION" +printf '1.19.0\n' >"$SMK/pithead-v1.19.0/VERSION" +ln -sfn "$SMK/pithead-v1.19.0" "$SMK/current" +smoke_resolve() { # + ( + _arg="$1" # saved before `set --`, which release-smoke's own arg parser needs empty + cd "$ROOT" || exit # its top level insists on a git repo, like the real invocation + set -- + # shellcheck disable=SC1090 + source "$ROOT/scripts/release-smoke.sh" 2>/dev/null + set +eu + upgraded_install_dir "$_arg" + ) +} +# Handed the previous VERSIONED dir — the shape that produced the false red. It is unchanged by +# design, so the answer has to come from the `current` beside it. +assert_eq "a versioned dir resolves to where the upgrade actually landed" \ + "$(tr -d '[:space:]' <"$(smoke_resolve "$SMK/pithead-v1.18.1")/VERSION")" "1.19.0" +# Handed the SYMLINK — resolved now, after the upgrade moved it. This is why the v1.19.2 cut did +# not hit the false red, and it must keep working. +assert_eq "the current symlink resolves to the new install" \ + "$(tr -d '[:space:]' <"$(smoke_resolve "$SMK/current")/VERSION")" "1.19.0" +# Nothing moved: a box that is already on the target must resolve to itself, not wander off. +ln -sfn "$SMK/pithead-v1.18.1" "$SMK/current" +assert_eq "with current pointing at it, the same dir resolves to itself" \ + "$(smoke_resolve "$SMK/pithead-v1.18.1")" "$SMK/pithead-v1.18.1" +# NOTE, measured rather than assumed: replacing the `readlink -f` with the raw argument leaves all +# three assertions above GREEN. The sibling lookup is what fixes the false red; the readlink only +# normalises the path that lands in the pass and failure messages. Recorded here so the next reader +# does not mistake it for a covered behaviour. +rm -rf "$SMK" +unset SMK + echo "== unit: release.sh signs the promoted digests (#376) ==" # sign_images must sign the recorded manifest-LIST digest (repo@sha256:… — never the mutable tag, # never a per-arch child) with the box's key and no Rekor upload; the password never reaches argv. From 8fefe66573e7127398c3fd3cd0244eca27c666dc Mon Sep 17 00:00:00 2001 From: Vijit Singh Date: Tue, 18 Aug 2026 18:53:13 -0500 Subject: [PATCH 05/47] fix(test): source release-smoke through a variable, not a literal path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit shellcheck follows a literal source path that names another file in the same invocation — release-smoke pulls in release.sh, whose `local tool missing=()` then collides with this file's own scalar `missing` and fails lint-sh. The same trap as the earlier $REL fix; the tier-1 suite is green either way, so only `make lint-sh` catches it. Co-Authored-By: Claude Opus 5 --- tests/stack/run.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/stack/run.sh b/tests/stack/run.sh index 8f09ffae..5374d816 100755 --- a/tests/stack/run.sh +++ b/tests/stack/run.sh @@ -2351,17 +2351,21 @@ echo "== unit: release-smoke resolves the upgraded install at ASSERT time (#1068 # upgrade reported as a failure, on the documented final gate of a release. MUTATION PROOF: return # the given path unresolved and both "lands on" assertions go red. SMK="$SANDBOX/smoke1068" +SMOKE_SH="$ROOT/scripts/release-smoke.sh" mkdir -p "$SMK/pithead-v1.18.1" "$SMK/pithead-v1.19.0" printf '1.18.1\n' >"$SMK/pithead-v1.18.1/VERSION" printf '1.19.0\n' >"$SMK/pithead-v1.19.0/VERSION" ln -sfn "$SMK/pithead-v1.19.0" "$SMK/current" smoke_resolve() { # ( - _arg="$1" # saved before `set --`, which release-smoke's own arg parser needs empty + _arg="$1" # saved before `set --`, which release-smoke's own arg parser needs empty cd "$ROOT" || exit # its top level insists on a git repo, like the real invocation set -- + # Sourced through a variable, never a literal path: shellcheck follows a literal that names + # another file in the same invocation, and release-smoke pulls in release.sh, whose + # `local tool missing=()` then collides with this file's own scalar `missing` (SC2178). # shellcheck disable=SC1090 - source "$ROOT/scripts/release-smoke.sh" 2>/dev/null + source "$SMOKE_SH" 2>/dev/null set +eu upgraded_install_dir "$_arg" ) @@ -2383,7 +2387,7 @@ assert_eq "with current pointing at it, the same dir resolves to itself" \ # normalises the path that lands in the pass and failure messages. Recorded here so the next reader # does not mistake it for a covered behaviour. rm -rf "$SMK" -unset SMK +unset SMK SMOKE_SH echo "== unit: release.sh signs the promoted digests (#376) ==" # sign_images must sign the recorded manifest-LIST digest (repo@sha256:… — never the mutable tag, From 39700e6d545bd63a203f01306c39ac3464532f64 Mon Sep 17 00:00:00 2001 From: Vijit Singh Date: Tue, 18 Aug 2026 22:20:34 -0500 Subject: [PATCH 06/47] fix(dashboard): stop the :80 redirect trusting the Host header (#1123) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Caddy's built-in HTTP->HTTPS redirect is a catch-all whose target is the Host header the request carried, so :80 answered `Host: evil.example` with `308 -> https://evil.example` — measured against a running box, not read off the config. That is the open redirector #1118 closed in the setup wizard, except this one is the state a machine spends its life in rather than a one-shot, and the landing point is the dashboard login: the screen where the operator types the dashboard password. Reaching it needs a name that resolves to the box, which is the threat model serving TLS on a LAN already accepts — so hardening, not an emergency, but permanent rather than one-shot. The render takes :80 over instead of leaving it to auto_https: emit `auto_https disable_redirects` and serve one site whose target is fixed to the address this box answers to. A catch-all rather than a site matching only $HOST_IP, deliberately — Caddy answers an unmatched host with an empty 200, so a refusal on :80 reads as a dead machine, and every legitimate request still lands on the dashboard. A custom HOST_PORT is untouched: :80 is deliberately left free for the fronting proxy co-hosting exists for (#740). Plain-HTTP mode has no redirect to steer. The block is a literal, not $(printf ...) — command substitution strips the trailing newlines and the next site block landed on the same line as this one's closing brace, which Caddy refuses. Same trap the $auth line documents from the other direction. Proven against the pinned caddy:2.11.4, not just against string matching: `caddy validate` reports Valid on all four rendered shapes, and a real Caddy serving the default answers `Host: evil.example` with `308 -> https://box.lan/setup` — path preserved, host no longer attacker-chosen. Coverage: five assertions at tier 1, the load-bearing one stated as the defect rather than the shape of the fix — no redir line may interpolate {host} or {http.request.host}. Mutations named in the test comment and run. Closes #1123 Co-Authored-By: Claude Opus 5 --- pithead | 30 +++++++++++++++++++++++++++++- tests/stack/run.sh | 44 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/pithead b/pithead index 7569ba8e..9fc6a163 100755 --- a/pithead +++ b/pithead @@ -4464,7 +4464,7 @@ generate_caddyfile() { # host can keep 80/443 (co-hosting, #181). In HTTPS mode a custom port also disables Caddy's # automatic :80 -> HTTPS redirect (a global option emitted once at the top of the file) so port # 80 is left free for that proxy; the fronting proxy owns any http->https bounce instead. - local dport="${HOST_PORT:-}" port_suffix="" global_block="" + local dport="${HOST_PORT:-}" port_suffix="" global_block="" redirect_block="" if [ "$DASHBOARD_SECURE" == "true" ]; then [ "$dport" == "443" ] && dport="" if [ -n "$dport" ]; then @@ -4474,6 +4474,33 @@ generate_caddyfile() { } ' + else + # Caddy's own HTTP->HTTPS redirect is a CATCH-ALL, and its target is the Host header + # the request carried: :80 answered `Host: evil.example` with `308 -> https://evil.example` + # (#1123, measured against a running box, not read off the config). That is the same + # open redirector #1118 closed in the setup wizard, except this one is the state the + # machine spends its life in, and it lands the operator on a page that looks like the + # dashboard login — the screen where they type the dashboard password. + # + # So take :80 over rather than leaving it to auto_https: disable the built-in redirects + # and serve one site whose target is fixed to the address this box answers to. The Host + # header stops being able to steer anything, and every legitimate :80 request — bare + # name, bare IP, whatever the operator typed — still lands on the dashboard, which a + # site block matching only $HOST_IP would not do (Caddy answers an unmatched host with + # an empty 200, so a plain refusal here reads as a dead machine). + global_block='{ + auto_https disable_redirects +} + +' + # A literal, NOT $(printf ...): command substitution strips the trailing newlines and + # the next site block lands on the same line as this one's closing brace, which Caddy + # refuses — the same trap the $auth line documents from the other direction. + redirect_block="http:// { + redir https://$HOST_IP{uri} 308 +} + +" fi else [ "$dport" == "80" ] && dport="" @@ -4482,6 +4509,7 @@ generate_caddyfile() { : >"Caddyfile" [ -n "$global_block" ] && printf '%s' "$global_block" >>"Caddyfile" + [ -n "$redirect_block" ] && printf '%s' "$redirect_block" >>"Caddyfile" if [ "$DASHBOARD_SECURE" == "true" ]; then log "Generating Caddyfile for automatic HTTPS ($HOST_IP$port_suffix)$([ -n "$auth" ] && echo ' with login')..." cat <>"Caddyfile" diff --git a/tests/stack/run.sh b/tests/stack/run.sh index 5374d816..5ad862f5 100755 --- a/tests/stack/run.sh +++ b/tests/stack/run.sh @@ -1505,8 +1505,9 @@ case "$caddy_port_http" in *"disable_redirects"*) bad "plain-HTTP custom port has no redirect global" "'disable_redirects' present on a plain-HTTP site" ;; *) ok "plain-HTTP custom port has no redirect global" ;; esac -# A port that equals the scheme default (443 secure / unset) renders today's Caddyfile verbatim — -# no port suffix, no redirect global. Guards the byte-identical default path. +# A port that equals the scheme default (443 secure / unset) renders the same site address as an +# unset one — no port suffix. It still takes :80 over from auto_https (see #1123 below), which is +# what separates it from the custom-port case: there, :80 is deliberately left to a fronting proxy. # shellcheck disable=SC1090 # STACK path is dynamic by design caddy_port_default="$( cd "$SANDBOX" && source "$STACK" 2>/dev/null @@ -1516,10 +1517,47 @@ caddy_port_default="$( )" assert_contains "explicit default port keeps the bare site address" "$caddy_port_default" "https://box.lan {" case "$caddy_port_default" in -*"disable_redirects"*) bad "default port emits no redirect global" "'disable_redirects' present at the scheme default" ;; *":443"*) bad "default port emits no explicit suffix" "':443' suffix present at the scheme default" ;; *) ok "default port renders the bare site address" ;; esac + +echo "== unit: the :80 redirect cannot be steered by the Host header (#1123) ==" +# Caddy's built-in HTTP->HTTPS redirect is a CATCH-ALL whose target is the request's own Host +# header, so a provisioned box answered `Host: evil.example` on :80 with `308 -> https://evil.example` +# — measured on a running machine. That is #1118's open redirector again, in the state the machine +# spends its life in, and it lands on the screen where the operator types the dashboard password. +# The render now disables the built-in redirects and serves :80 itself with a FIXED target. +# MUTATION PROOF: render `redir https://{host}{uri}` instead of the literal host, or drop the +# `auto_https disable_redirects`, and the two assertions below go red. +# shellcheck disable=SC1090 # STACK path is dynamic by design +caddy_redir="$( + cd "$SANDBOX" && source "$STACK" 2>/dev/null + set +e + DASHBOARD_SECURE=true HOST_IP=box.lan DASHBOARD_AUTH_HASH_B64="" generate_caddyfile >/dev/null 2>&1 + cat Caddyfile +)" +assert_contains "the default secure render takes :80 over from auto_https" "$caddy_redir" "auto_https disable_redirects" +assert_contains "it serves :80 itself" "$caddy_redir" "http:// {" +assert_contains "and redirects to the box, by name, not to whatever was asked for" "$caddy_redir" \ + "redir https://box.lan{uri} 308" +# The whole defect in one assertion: no redirect target may be built from the request. Caddy spells +# the request's host `{host}` (and `{http.request.host}`), so neither may appear in a redir line. +caddy_redir_lines="$(printf '%s\n' "$caddy_redir" | grep -a "redir" || true)" +case "$caddy_redir_lines" in +*"{host}"* | *"{http.request.host}"*) bad "no redirect target comes from the request" "a redir line interpolates the request host: $caddy_redir_lines" ;; +*) ok "no redirect target comes from the request" ;; +esac +# A custom port means a fronting proxy owns :80 (#740). Taking it over there would break exactly the +# co-hosting the option exists for, so the catch-all is the default path's alone. +case "$caddy_port_https" in +*"http:// {"*) bad "a custom port leaves :80 to the fronting proxy" "the render claimed :80 anyway" ;; +*) ok "a custom port leaves :80 to the fronting proxy" ;; +esac +# Plain-HTTP mode has no redirect to steer: :80 IS the dashboard there. +case "$caddy_http" in +*"redir"*) bad "plain-HTTP mode renders no redirect at all" "a redir line appeared on a plain-HTTP site" ;; +*) ok "plain-HTTP mode renders no redirect at all" ;; +esac # Onion + custom LAN port together (#740 × #343): the LAN vhost moves to the custom port and the # `disable_redirects` global is emitted, but the onion vhost MUST stay on the bridge gateway's bare # :80 — Tor's HiddenServicePort maps 80 -> NETWORK_PREFIX.1:80, so a custom LAN port must not leak From 2113cbb87ec0888069d1fa8d83ea93025c9caca3 Mon Sep 17 00:00:00 2001 From: Vijit Singh Date: Tue, 18 Aug 2026 22:38:48 -0500 Subject: [PATCH 07/47] fix(upgrade): tell the operator a spent GitHub rate limit is not a dead Tor circuit (#1081) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The one-click upgrade was rejected on a healthy bench box with 'could not reach the GitHub release API over Tor'. Tor was fine and the dial landed; GitHub answered 403 because its unauthenticated limit is 60 requests an hour PER IP and a Tor exit is shared with everyone else using it, so the budget had been spent by strangers. curl -f collapses every non-2xx into one exit code, so the only thing the operator was told pointed at './pithead doctor' — which correctly reports Tor egress healthy, because it is. They are sent to a check that will tell them nothing is wrong, about a box where nothing is wrong. The remedy is a different one entirely: './pithead restart tor' picks fresh guards and lands on another exit with its own budget. That worked immediately on the bench, and nothing in the message pointed at it. Both release lookups — the pithead one and the RigForge one — now go through a single gh_release_fetch. Two copies of the same curl call are how one message could have been fixed and the other left wrong, and a tier-1 assertion counts the callers so a third copy cannot appear. NOT done, deliberately: the issue also suggests releasing the 10-minute throttle on a rate-limit refusal so the operator can act at once. The throttle is claimed before the dial so a compromised container cannot use failed attempts as an unthrottled GitHub/Tor beacon, and a rate-limited request DID reach GitHub — so releasing it restores exactly the beacon the throttle exists to stop. The message now tells the operator to restart Tor, which is the useful thing to do during the window they have to wait out anyway. Coverage: a stubbed curl drives all four outcomes — 200, a 403 naming the rate limit, a transport failure, and a 500 — and asserts in BOTH directions, so 'always blame the rate limit' fails as loudly as 'never blame it'. Plus the two counter assertions that keep the callers on the shared function. Closes #1081 Co-Authored-By: Claude Opus 5 --- pithead | 66 +++++++++++++++++++++++++++++++++++----------- tests/stack/run.sh | 62 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 16 deletions(-) diff --git a/pithead b/pithead index 7569ba8e..e9b6828e 100755 --- a/pithead +++ b/pithead @@ -3340,6 +3340,50 @@ readonly CURL_CAP_SMALL=1048576 # 1 MiB — GitHub release JSON, rig control res # cap-hit surfaces as _upg_fail's "could not download over Tor", loud but network-flavoured). readonly CURL_CAP_BUNDLE=16777216 # 16 MiB — the pithead.tar.gz release bundle (code + configs) +# The latest-release JSON for a GitHub repo, over the stack's own Tor SOCKS like every other +# stack egress. Prints the JSON and returns 0; on failure prints nothing, returns 1, and leaves +# GH_RELEASE_HINT holding the sentence the operator should actually be told. +# +# The reason this is a function and not two `curl -fsS` calls: `-f` collapses every non-2xx into +# one exit code, so a 403 came out as "could not reach GitHub over Tor" and sent the operator to a +# doctor run that correctly reports Tor healthy. GitHub's unauthenticated limit is 60 requests an +# hour PER IP, and a Tor exit is shared with everyone else using it, so a spent budget is a normal +# condition with nothing wrong with this machine — and a different remedy (pick a new exit) from a +# dial that genuinely failed. +GH_RELEASE_HINT="" +gh_release_fetch() { # -> release JSON on stdout + local repo="$1" prefix socks out code + prefix=$(env_get NETWORK_PREFIX 2>/dev/null) || true + [ -n "$prefix" ] || prefix="172.28.0" + socks="${prefix}.25:9050" + GH_RELEASE_HINT="" + # -w appends the status on its own line, so a non-2xx keeps its BODY — which is where GitHub + # says the limit was exceeded. Without -f, curl's own non-zero exit now means only a transport + # failure, which is exactly the distinction that was missing. + if ! out=$(curl -sS --max-time 60 --max-filesize "$CURL_CAP_SMALL" -w '\n%{http_code}' \ + --socks5-hostname "$socks" -H 'Accept: application/vnd.github+json' \ + "https://api.github.com/repos/$repo/releases/latest" 2>/dev/null); then + GH_RELEASE_HINT="could not reach the GitHub release API over Tor — nothing was changed. Check './pithead doctor' and retry." + return 1 + fi + code=${out##*$'\n'} + out=${out%$'\n'*} + case "$code" in + 2*) + printf '%s' "$out" + return 0 + ;; + 403 | 429) + if printf '%s' "$out" | grep -qi 'rate limit'; then + GH_RELEASE_HINT="the Tor exit this machine is currently using has spent GitHub's shared hourly request budget — nothing is wrong with this box, and 'doctor' will say so. Run './pithead restart tor' to pick a new exit, then retry." + return 1 + fi + ;; + esac + GH_RELEASE_HINT="the GitHub release API answered HTTP $code — nothing was changed. Retry in a few minutes." + return 1 +} + # Render the pre-masked prefill copy (#440): the live config with every SET secret leaf replaced # by the {"__secret__":true} sentinel, written atomically to /masked/config.json. # The dashboard serves the Configuration form from THIS file (mounted read-only) — the raw @@ -6040,14 +6084,9 @@ control_upgrade() { # # is the right trade. touch "$stamp" # Host-side re-derivation of the target: the latest tag according to GitHub, not the request. - local prefix socks rel tag - prefix=$(env_get NETWORK_PREFIX 2>/dev/null) - [ -n "$prefix" ] || prefix="172.28.0" - socks="${prefix}.25:9050" - if ! rel=$(curl -fsS --max-time 60 --max-filesize "$CURL_CAP_SMALL" --socks5-hostname "$socks" \ - -H 'Accept: application/vnd.github+json' \ - "https://api.github.com/repos/p2pool-starter-stack/pithead/releases/latest" 2>/dev/null); then - _upg_reject "could not reach the GitHub release API over Tor — nothing was changed. Check './pithead doctor' and retry." + local rel tag + if ! rel=$(gh_release_fetch p2pool-starter-stack/pithead); then + _upg_reject "$GH_RELEASE_HINT" return 0 fi tag=$(printf '%s' "$rel" | jq -r '.tag_name // ""' 2>/dev/null) @@ -6552,14 +6591,9 @@ control_worker_upgrade() { # return 0 fi touch "$stampf" - local prefix socks rel - prefix=$(env_get NETWORK_PREFIX 2>/dev/null) - [ -n "$prefix" ] || prefix="172.28.0" - socks="${prefix}.25:9050" - if ! rel=$(curl -fsS --max-time 60 --max-filesize "$CURL_CAP_SMALL" --socks5-hostname "$socks" \ - -H 'Accept: application/vnd.github+json' \ - "https://api.github.com/repos/p2pool-starter-stack/rigforge/releases/latest" 2>/dev/null); then - _wu_reject "could not reach the GitHub release API over Tor — nothing was changed. Check './pithead doctor' and retry." + local rel + if ! rel=$(gh_release_fetch p2pool-starter-stack/rigforge); then + _wu_reject "$GH_RELEASE_HINT" return 0 fi tag=$(printf '%s' "$rel" | jq -r '.tag_name // ""' 2>/dev/null) diff --git a/tests/stack/run.sh b/tests/stack/run.sh index 5374d816..319b1dbc 100755 --- a/tests/stack/run.sh +++ b/tests/stack/run.sh @@ -6303,6 +6303,68 @@ out="$(run_pending)" assert_contains "next run drains the remainder" "$out" "Processed 10 control request(s)" assert_eq "spool empty after the second run" "$(ls "$REQS" | wc -l | tr -d ' ')" "0" +echo "== unit: a spent GitHub rate limit is not a dead Tor circuit (#1081) ==" +# The one-click upgrade was rejected on a healthy box with "could not reach the GitHub release API +# over Tor". Tor was fine and the dial succeeded; GitHub answered 403 because the unauthenticated +# limit is 60 requests an hour PER IP and a Tor exit is shared with everyone else using it, so the +# budget had been spent by strangers. `curl -f` collapses every non-2xx into one exit code, so the +# only thing the operator was told pointed at 'doctor' — which correctly reports Tor healthy. +# +# The remedy is a different one entirely: pick a new exit. The fetch is now ONE function both the +# pithead and the RigForge lookups go through, so neither can drift back. +# +# MUTATION PROOF: make the 403 branch fall through to the generic hint (or restore `curl -fsS`) and +# the rate-limit assertion goes red; the transport-failure assertion holds it honest in the other +# direction, so "always blame the rate limit" does not pass either. +GHR="$SANDBOX/gh-release" +mkdir -p "$GHR/bin" +cat >"$GHR/bin/curl" <<'EOF' +#!/usr/bin/env bash +# Answers with $GH_STUB_CODE and $GH_STUB_BODY, in the shape `-w '\n%{http_code}'` produces. +[ "${GH_STUB_TRANSPORT_FAIL:-0}" = "1" ] && exit 7 +printf '%s\n%s' "${GH_STUB_BODY:-}" "${GH_STUB_CODE:-200}" +EOF +chmod +x "$GHR/bin/curl" +gh_fetch() { # [transport-fail] -> "||" + ( + cd "$GHR" && source "$STACK" 2>/dev/null + set +e + export PATH="$GHR/bin:$PATH" GH_STUB_CODE="$1" GH_STUB_BODY="$2" GH_STUB_TRANSPORT_FAIL="${3:-0}" + out=$(gh_release_fetch p2pool-starter-stack/pithead) + printf '%s|%s|%s' "$?" "$out" "$GH_RELEASE_HINT" + ) +} +gh_ok=$(gh_fetch 200 '{"tag_name":"v1.2.3"}') +assert_eq "a 200 returns the release JSON" "${gh_ok%%|*}" "0" +assert_contains "and the JSON is what the caller gets" "$gh_ok" '"tag_name":"v1.2.3"' + +gh_rl=$(gh_fetch 403 '{"message":"API rate limit exceeded for 203.0.113.9."}') +assert_eq "a spent rate limit is a failure" "${gh_rl%%|*}" "1" +assert_contains "and names the remedy that actually works" "$gh_rl" "restart tor" +case "$gh_rl" in +*"could not reach the GitHub release API"*) bad "a spent rate limit is not reported as unreachable" "the hint still blames the dial: $gh_rl" ;; +*) ok "a spent rate limit is not reported as unreachable" ;; +esac + +gh_tf=$(gh_fetch 000 '' 1) +assert_eq "a transport failure is still a failure" "${gh_tf%%|*}" "1" +assert_contains "and still reads as a dial that did not land" "$gh_tf" "could not reach the GitHub release API" +case "$gh_tf" in +*"restart tor"*) bad "a dial failure is not blamed on the rate limit" "the hint sends them to restart tor: $gh_tf" ;; +*) ok "a dial failure is not blamed on the rate limit" ;; +esac + +gh_500=$(gh_fetch 500 'upstream is unwell') +assert_eq "a server error is a failure" "${gh_500%%|*}" "1" +assert_contains "and says which status came back" "$gh_500" "HTTP 500" + +# Both lookups go through the one function — a second copy is how the two messages drifted apart in +# the first place, and the RigForge one would have kept the old wrong hint. +assert_eq "both release lookups use the shared fetch" \ + "$(grep -c 'gh_release_fetch p2pool-starter-stack/' "$STACK")" "2" +assert_eq "no release lookup dials the API directly any more" \ + "$(grep -c 'api.github.com/repos/.*/releases/latest' "$STACK")" "1" + echo "== black-box: control upgrade verb (#59) ==" # A RELEASE install (no build/*/Dockerfile → is_source_checkout false) with the control channel # on. The runner's upgrade verb runs against a stub curl (GitHub release API + bundle download) From 50ab5f8df9b64c6056cd63cb6003f810d202ce84 Mon Sep 17 00:00:00 2001 From: Vijit Singh Date: Tue, 18 Aug 2026 22:40:43 -0500 Subject: [PATCH 08/47] test: teach the shared upgrade curl stub to answer with a status line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The release lookup reads the body AND the status now, so a stub that answers with the bare JSON leaves gh_release_fetch parsing the last line of that JSON as the HTTP code — every #59 control-upgrade test would have failed on a change that is about messages. GH_STUB_CODE is there so a later test can drive a real non-2xx through the control path rather than only through the unit. Co-Authored-By: Claude Opus 5 --- tests/stack/run.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/stack/run.sh b/tests/stack/run.sh index 319b1dbc..8a422cd5 100755 --- a/tests/stack/run.sh +++ b/tests/stack/run.sh @@ -6410,7 +6410,13 @@ for a in "$@"; do prev="$a" done case "$url" in -*api.github.com*) cat "${CURL_API_RESPONSE:?}" ;; +# The release lookup reads the body AND the status now (#1081), so the stub has to answer in the +# shape `-w '\n%{http_code}'` produces. GH_STUB_CODE lets a test drive a non-2xx through the real +# control path; unset means the ordinary 200. +*api.github.com*) + cat "${CURL_API_RESPONSE:?}" + printf '\n%s' "${GH_STUB_CODE:-200}" + ;; *releases/download/*.sig) cp "${CURL_SIG:?}" "$out" ;; *releases/download/*) cp "${CURL_BUNDLE:?}" "$out" ;; *) exit 22 ;; From 7c5ea28f3b1b8b385f7e4817b201fae0c9be0b3e Mon Sep 17 00:00:00 2001 From: Vijit Singh Date: Tue, 18 Aug 2026 22:55:31 -0500 Subject: [PATCH 09/47] =?UTF-8?q?fix(upgrade):=20the=20hint=20has=20to=20r?= =?UTF-8?q?each=20the=20caller=20=E2=80=94=20a=20command=20substitution=20?= =?UTF-8?q?swallowed=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adversarial review of my own branch. `rel=$(gh_release_fetch ...)` reads naturally and is a subshell, so GH_RELEASE_HINT was set inside it and discarded: every rejection would have carried an EMPTY message. That is the defect this change exists to remove — a message that tells the operator nothing — put back by the refactor that removes it, and it would have shipped looking correct. The fetch now sets GH_RELEASE_JSON and is called as a plain command, so both globals land in the caller's shell. A tier-1 assertion refuses any caller that wraps it in a command substitution, because the wrong form is the one that reads better. Co-Authored-By: Claude Opus 5 --- pithead | 16 ++++++++++++---- tests/stack/run.sh | 10 ++++++++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/pithead b/pithead index fa80d03d..8c740875 100755 --- a/pithead +++ b/pithead @@ -3350,13 +3350,19 @@ readonly CURL_CAP_BUNDLE=16777216 # 16 MiB — the pithead.tar.gz release bundle # hour PER IP, and a Tor exit is shared with everyone else using it, so a spent budget is a normal # condition with nothing wrong with this machine — and a different remedy (pick a new exit) from a # dial that genuinely failed. +# NOT stdout, deliberately: the JSON lands in GH_RELEASE_JSON and the caller runs this as a plain +# command. `rel=$(gh_release_fetch ...)` reads naturally and is WRONG — command substitution is a +# subshell, so the hint set here would never reach the caller and every rejection would carry an +# empty message. Two globals, one call, no subshell. GH_RELEASE_HINT="" -gh_release_fetch() { # -> release JSON on stdout +GH_RELEASE_JSON="" +gh_release_fetch() { # ; sets GH_RELEASE_JSON on success, GH_RELEASE_HINT on failure local repo="$1" prefix socks out code prefix=$(env_get NETWORK_PREFIX 2>/dev/null) || true [ -n "$prefix" ] || prefix="172.28.0" socks="${prefix}.25:9050" GH_RELEASE_HINT="" + GH_RELEASE_JSON="" # -w appends the status on its own line, so a non-2xx keeps its BODY — which is where GitHub # says the limit was exceeded. Without -f, curl's own non-zero exit now means only a transport # failure, which is exactly the distinction that was missing. @@ -3370,7 +3376,7 @@ gh_release_fetch() { # -> release JSON on stdout out=${out%$'\n'*} case "$code" in 2*) - printf '%s' "$out" + GH_RELEASE_JSON="$out" return 0 ;; 403 | 429) @@ -6113,10 +6119,11 @@ control_upgrade() { # touch "$stamp" # Host-side re-derivation of the target: the latest tag according to GitHub, not the request. local rel tag - if ! rel=$(gh_release_fetch p2pool-starter-stack/pithead); then + if ! gh_release_fetch p2pool-starter-stack/pithead; then _upg_reject "$GH_RELEASE_HINT" return 0 fi + rel=$GH_RELEASE_JSON tag=$(printf '%s' "$rel" | jq -r '.tag_name // ""' 2>/dev/null) if ! printf '%s' "$tag" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then _upg_reject "the GitHub release API returned no usable release tag — nothing was changed." @@ -6620,10 +6627,11 @@ control_worker_upgrade() { # fi touch "$stampf" local rel - if ! rel=$(gh_release_fetch p2pool-starter-stack/rigforge); then + if ! gh_release_fetch p2pool-starter-stack/rigforge; then _wu_reject "$GH_RELEASE_HINT" return 0 fi + rel=$GH_RELEASE_JSON tag=$(printf '%s' "$rel" | jq -r '.tag_name // ""' 2>/dev/null) if ! printf '%s' "$tag" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then _wu_reject "the GitHub release API returned no usable RigForge release tag — nothing was changed." diff --git a/tests/stack/run.sh b/tests/stack/run.sh index 9e4f16a0..59c757a6 100755 --- a/tests/stack/run.sh +++ b/tests/stack/run.sh @@ -6368,8 +6368,8 @@ gh_fetch() { # [transport-fail] -> "||" cd "$GHR" && source "$STACK" 2>/dev/null set +e export PATH="$GHR/bin:$PATH" GH_STUB_CODE="$1" GH_STUB_BODY="$2" GH_STUB_TRANSPORT_FAIL="${3:-0}" - out=$(gh_release_fetch p2pool-starter-stack/pithead) - printf '%s|%s|%s' "$?" "$out" "$GH_RELEASE_HINT" + gh_release_fetch p2pool-starter-stack/pithead + printf '%s|%s|%s' "$?" "$GH_RELEASE_JSON" "$GH_RELEASE_HINT" ) } gh_ok=$(gh_fetch 200 '{"tag_name":"v1.2.3"}') @@ -6402,6 +6402,12 @@ assert_eq "both release lookups use the shared fetch" \ "$(grep -c 'gh_release_fetch p2pool-starter-stack/' "$STACK")" "2" assert_eq "no release lookup dials the API directly any more" \ "$(grep -c 'api.github.com/repos/.*/releases/latest' "$STACK")" "1" +# The hint has to reach the CALLER. `rel=$(gh_release_fetch ...)` reads naturally and is a subshell, +# so the hint would be set and discarded and every rejection would carry an empty message — the +# defect this whole change exists to remove, reintroduced by the refactor that removes it. Neither +# caller may wrap the fetch in a command substitution. +assert_eq "neither caller swallows the hint in a subshell" \ + "$(grep -c '=\$(gh_release_fetch' "$STACK")" "0" echo "== black-box: control upgrade verb (#59) ==" # A RELEASE install (no build/*/Dockerfile → is_source_checkout false) with the control channel From 5917bd84e090bce316fffc78dbf929e4a04c0242 Mon Sep 17 00:00:00 2001 From: Vijit Singh Date: Wed, 19 Aug 2026 00:00:06 -0500 Subject: [PATCH 10/47] fix(upgrade): the fetch publishes the SOCKS address, so the downloads still have one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The suite caught what the adversarial read did not. Folding the two release lookups into gh_release_fetch deleted control_upgrade's own `local prefix socks` derivation — but two later curls in that same function still said "$socks". Under `set -u` the runner died at its first download: the upgrade result sat at "running" for ever, one dial in the log instead of two, nothing extracted, and 60 assertions went red together while the seven new unit assertions all passed. That is the shape worth remembering. The unit tests exercised the new function and said it worked, and it did. The damage was to the caller the refactor emptied out, and only the integration block could see it. The fetch now publishes GH_SOCKS and every dial on that path reads the same address, so the derivation happens once instead of twice. Two assertions pin it: three dials use GH_SOCKS, and none refers to a socks variable nobody declares. Also fixes the subshell assertion, which used `grep -c` with a BRE where \( opens a group — it errored rather than matching, and reported red on a clean tree. -F. Co-Authored-By: Claude Opus 5 --- pithead | 15 ++++++++++----- tests/stack/run.sh | 11 ++++++++++- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/pithead b/pithead index 8c740875..5df7627a 100755 --- a/pithead +++ b/pithead @@ -3356,18 +3356,23 @@ readonly CURL_CAP_BUNDLE=16777216 # 16 MiB — the pithead.tar.gz release bundle # empty message. Two globals, one call, no subshell. GH_RELEASE_HINT="" GH_RELEASE_JSON="" +# The SOCKS address this fetch used, published so a caller that goes on to download the release over +# the same Tor path derives it ONCE rather than twice. Folding the lookup into a function deleted the +# caller's own derivation, and `set -u` then killed the runner at its first download — the upgrade +# result sat at "running" for ever with a single dial in the log. One derivation, one truth. +GH_SOCKS="" gh_release_fetch() { # ; sets GH_RELEASE_JSON on success, GH_RELEASE_HINT on failure - local repo="$1" prefix socks out code + local repo="$1" prefix out code prefix=$(env_get NETWORK_PREFIX 2>/dev/null) || true [ -n "$prefix" ] || prefix="172.28.0" - socks="${prefix}.25:9050" + GH_SOCKS="${prefix}.25:9050" GH_RELEASE_HINT="" GH_RELEASE_JSON="" # -w appends the status on its own line, so a non-2xx keeps its BODY — which is where GitHub # says the limit was exceeded. Without -f, curl's own non-zero exit now means only a transport # failure, which is exactly the distinction that was missing. if ! out=$(curl -sS --max-time 60 --max-filesize "$CURL_CAP_SMALL" -w '\n%{http_code}' \ - --socks5-hostname "$socks" -H 'Accept: application/vnd.github+json' \ + --socks5-hostname "$GH_SOCKS" -H 'Accept: application/vnd.github+json' \ "https://api.github.com/repos/$repo/releases/latest" 2>/dev/null); then GH_RELEASE_HINT="could not reach the GitHub release API over Tor — nothing was changed. Check './pithead doctor' and retry." return 1 @@ -6141,7 +6146,7 @@ control_upgrade() { # control_audit "$cdir/audit/control.log" "$id" "$actor" "upgrade" "started" # Bundle URL built from the HOST-derived tag only; fetched over the same Tor SOCKS. local bundle="$cdir/staged/.$id.tar.gz" logf="$cdir/staged/.$id.log" - if ! curl -fsSL --max-time 900 --max-filesize "$CURL_CAP_BUNDLE" --socks5-hostname "$socks" -o "$bundle" \ + if ! curl -fsSL --max-time 900 --max-filesize "$CURL_CAP_BUNDLE" --socks5-hostname "$GH_SOCKS" -o "$bundle" \ "https://github.com/p2pool-starter-stack/pithead/releases/download/$tag/pithead.tar.gz" 2>/dev/null; then rm -f "$bundle" _upg_fail "could not download the $tag release bundle over Tor — the stack keeps running v$PITHEAD_VERSION." @@ -6154,7 +6159,7 @@ control_upgrade() { # # TLS to GitHub plus tag pinning — with one loud line in the journal. if [ -f cosign.pub ]; then local sig="$cdir/staged/.$id.tar.gz.sig" - if ! curl -fsSL --max-time 120 --max-filesize "$CURL_CAP_SMALL" --socks5-hostname "$socks" -o "$sig" \ + if ! curl -fsSL --max-time 120 --max-filesize "$CURL_CAP_SMALL" --socks5-hostname "$GH_SOCKS" -o "$sig" \ "https://github.com/p2pool-starter-stack/pithead/releases/download/$tag/pithead.tar.gz.sig" 2>/dev/null; then rm -f "$bundle" "$sig" _upg_fail "the $tag release carries no bundle signature (pithead.tar.gz.sig) — refusing to install it unverified (#376); the stack keeps running v$PITHEAD_VERSION." diff --git a/tests/stack/run.sh b/tests/stack/run.sh index 59c757a6..4b3d9609 100755 --- a/tests/stack/run.sh +++ b/tests/stack/run.sh @@ -6407,7 +6407,16 @@ assert_eq "no release lookup dials the API directly any more" \ # defect this whole change exists to remove, reintroduced by the refactor that removes it. Neither # caller may wrap the fetch in a command substitution. assert_eq "neither caller swallows the hint in a subshell" \ - "$(grep -c '=\$(gh_release_fetch' "$STACK")" "0" + "$(grep -cF '=$(gh_release_fetch' "$STACK")" "0" +# And the other half of the same mistake: folding the lookup into a function DELETED the caller's own +# `local prefix socks` derivation, while two later downloads in that same function still said +# "$socks". Under `set -u` the runner died at its first download — the upgrade result sat at +# "running" for ever with a single dial in the log and nothing extracted, and 60 assertions went red +# together. The fetch publishes the address it used; every dial on that path reads the same one. +assert_eq "every dial on the upgrade path uses the address the lookup derived" \ + "$(grep -cF -- '--socks5-hostname "$GH_SOCKS"' "$STACK")" "3" +assert_eq "no dial refers to a socks variable nobody declares" \ + "$(grep -cF -- '--socks5-hostname "$socks"' "$STACK")" "0" echo "== black-box: control upgrade verb (#59) ==" # A RELEASE install (no build/*/Dockerfile → is_source_checkout false) with the control channel From 24b69b4802d3d21e86b41b84567c671d44e653b3 Mon Sep 17 00:00:00 2001 From: Vijit Singh Date: Wed, 19 Aug 2026 00:23:15 -0500 Subject: [PATCH 11/47] test: the RigForge lookup's stub owes a status line too, and my comment tripped my own guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two of the four remaining failures, both in checks rather than code. The worker-upgrade stub answered */releases/latest with bare JSON while already emitting 202 and 200 for the endpoints the product reads with -w. The release lookup now reads a status too, so that branch owed one — three RigForge assertions were failing on a stub that predated the change. And the subshell guard was matching the sentence in pithead explaining the very mistake it exists to prevent: the comment quoted the wrong form literally, so grep counted it. The guard was right; the prose was the problem. Reworded rather than weakening the assertion to skip comments — a guard that ignores comment lines is a guard someone can disable with a comment. Co-Authored-By: Claude Opus 5 --- pithead | 2 +- tests/stack/run.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pithead b/pithead index 5df7627a..5e8634fc 100755 --- a/pithead +++ b/pithead @@ -3351,7 +3351,7 @@ readonly CURL_CAP_BUNDLE=16777216 # 16 MiB — the pithead.tar.gz release bundle # condition with nothing wrong with this machine — and a different remedy (pick a new exit) from a # dial that genuinely failed. # NOT stdout, deliberately: the JSON lands in GH_RELEASE_JSON and the caller runs this as a plain -# command. `rel=$(gh_release_fetch ...)` reads naturally and is WRONG — command substitution is a +# command. Assigning it through a command substitution reads naturally and is WRONG — that is a # subshell, so the hint set here would never reach the caller and every rejection would carry an # empty message. Two globals, one call, no subshell. GH_RELEASE_HINT="" diff --git a/tests/stack/run.sh b/tests/stack/run.sh index 4b3d9609..13bba482 100755 --- a/tests/stack/run.sh +++ b/tests/stack/run.sh @@ -7620,7 +7620,7 @@ while [ $# -gt 0 ]; do esac done case "$url" in -*/releases/latest) printf '{"tag_name":"v9.9.9"}' ;; +*/releases/latest) printf '{"tag_name":"v9.9.9"}\n200' ;; */upgrade) printf '{"change_id":"chg-9"}' >"$out"; printf '202' ;; */status) printf '{"change_id":"chg-9","status":"applied"}' >"$out"; printf '200' ;; *) printf '000' ;; From ff71981c33085110a95f778b7fc95c90779c8e3d Mon Sep 17 00:00:00 2001 From: Vijit Singh Date: Wed, 19 Aug 2026 00:30:18 -0500 Subject: [PATCH 12/47] fix(upgrade): a response with no status line must not become the operator's message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The last failing assertion was a real finding, not a stub artefact. When the response carries no status line, `code=${out##*$'\n'}` has no newline to strip and `code` becomes the ENTIRE BODY — which then went straight into an operator-facing string: 'the GitHub release API answered HTTP {"message":"Not Found"}'. Nonsense to read, and a way for a remote body to land verbatim in the dashboard. A status that is not three digits is now its own case with its own message. The body never reaches the operator. The junk stub gets its status line too — it is simulating a 200 whose body has no usable tag, which is exactly what the assertion is about, and without the status it was simulating something the product can no longer receive. Co-Authored-By: Claude Opus 5 --- pithead | 10 ++++++++++ tests/stack/run.sh | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/pithead b/pithead index 5e8634fc..2f418e12 100755 --- a/pithead +++ b/pithead @@ -3379,6 +3379,16 @@ gh_release_fetch() { # ; sets GH_RELEASE_JSON on success, GH_RELEASE fi code=${out##*$'\n'} out=${out%$'\n'*} + # No status line at all means the response is not one we can reason about — and without this the + # WHOLE BODY becomes "$code" and gets echoed into an operator-facing message, which is both + # nonsense to read and a way for a remote body to land verbatim in the dashboard. + case "$code" in + [0-9][0-9][0-9]) ;; + *) + GH_RELEASE_HINT="the GitHub release API answered in a shape this cannot read — nothing was changed. Check './pithead doctor' and retry." + return 1 + ;; + esac case "$code" in 2*) GH_RELEASE_JSON="$out" diff --git a/tests/stack/run.sh b/tests/stack/run.sh index 13bba482..089e797e 100755 --- a/tests/stack/run.sh +++ b/tests/stack/run.sh @@ -7655,7 +7655,7 @@ mkdir -p "$ghjunk_dir/staged" "$ghjunk_dir/results" "$ghjunk_dir/audit" "$ghjunk cp "$WU/config.json" "$ghjunk_dir/config.json" cat >"$ghjunk_dir/bin/curl" <<'EOF' #!/usr/bin/env bash -printf '{"message":"Not Found"}' +printf '{"message":"Not Found"}\n200' exit 0 EOF chmod +x "$ghjunk_dir/bin/curl" From f0f502d240ac29259c14eedb815f9f3711a2bb20 Mon Sep 17 00:00:00 2001 From: Vijit Singh Date: Wed, 19 Aug 2026 00:54:31 -0500 Subject: [PATCH 13/47] test: drive the no-status-line case, since the hardening had no assertion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I added the three-digit guard in response to a failing test and left the guard itself uncovered — the assertion that went green afterwards only proved the stub now sends a status, not that the product handles a response without one. GH_STUB_NOCODE makes the stub answer the way that produced the defect, and the assertion is stated as the defect: the hint must not quote the response body. Mutation run — remove the guard and it goes red (1815/1). Co-Authored-By: Claude Opus 5 --- tests/stack/run.sh | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/stack/run.sh b/tests/stack/run.sh index 089e797e..f2015ba4 100755 --- a/tests/stack/run.sh +++ b/tests/stack/run.sh @@ -6360,6 +6360,7 @@ cat >"$GHR/bin/curl" <<'EOF' #!/usr/bin/env bash # Answers with $GH_STUB_CODE and $GH_STUB_BODY, in the shape `-w '\n%{http_code}'` produces. [ "${GH_STUB_TRANSPORT_FAIL:-0}" = "1" ] && exit 7 +[ "${GH_STUB_NOCODE:-0}" = "1" ] && { printf '%s' "${GH_STUB_BODY:-}"; exit 0; } printf '%s\n%s' "${GH_STUB_BODY:-}" "${GH_STUB_CODE:-200}" EOF chmod +x "$GHR/bin/curl" @@ -6392,6 +6393,22 @@ case "$gh_tf" in *) ok "a dial failure is not blamed on the rate limit" ;; esac +# No status line at all — `code` is then the WHOLE BODY, and it went into an operator-facing string +# ("answered HTTP {"message":"Not Found"}"): unreadable, and a way for a remote body to reach the +# dashboard verbatim. GH_STUB_NOCODE makes the stub answer the way that produced it. +gh_nocode=$( + cd "$GHR" && source "$STACK" 2>/dev/null + set +e + export PATH="$GHR/bin:$PATH" GH_STUB_BODY='{"message":"Not Found"}' GH_STUB_NOCODE=1 + gh_release_fetch p2pool-starter-stack/pithead + printf '%s|%s' "$?" "$GH_RELEASE_HINT" +) +assert_eq "a response with no status line is a failure" "${gh_nocode%%|*}" "1" +case "$gh_nocode" in +*'{"message"'* | *'Not Found'*) bad "the body never reaches the operator" "the hint quotes the response body: $gh_nocode" ;; +*) ok "the body never reaches the operator" ;; +esac + gh_500=$(gh_fetch 500 'upstream is unwell') assert_eq "a server error is a failure" "${gh_500%%|*}" "1" assert_contains "and says which status came back" "$gh_500" "HTTP 500" From 7479eb4c754a4f4eb4ed2b1032df6c7ee2792fa8 Mon Sep 17 00:00:00 2001 From: Vijit Singh Date: Wed, 19 Aug 2026 01:36:42 -0500 Subject: [PATCH 14/47] test: SC1090 directives on the two new sourced blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI caught what I did not: I ran shellcheck on pithead and not on the test file, so two `source "$STACK"` blocks went in without the disable directive every other block in this file carries. `make lint-sh` is the surface that sees this and the suite is not — the lesson the last session already wrote down, and I still only ran half of it. Co-Authored-By: Claude Opus 5 --- tests/stack/run.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/stack/run.sh b/tests/stack/run.sh index f2015ba4..9de05d0b 100755 --- a/tests/stack/run.sh +++ b/tests/stack/run.sh @@ -6366,7 +6366,9 @@ EOF chmod +x "$GHR/bin/curl" gh_fetch() { # [transport-fail] -> "||" ( - cd "$GHR" && source "$STACK" 2>/dev/null + cd "$GHR" || exit 1 + # shellcheck disable=SC1090 # STACK path is dynamic by design + source "$STACK" 2>/dev/null set +e export PATH="$GHR/bin:$PATH" GH_STUB_CODE="$1" GH_STUB_BODY="$2" GH_STUB_TRANSPORT_FAIL="${3:-0}" gh_release_fetch p2pool-starter-stack/pithead @@ -6397,7 +6399,9 @@ esac # ("answered HTTP {"message":"Not Found"}"): unreadable, and a way for a remote body to reach the # dashboard verbatim. GH_STUB_NOCODE makes the stub answer the way that produced it. gh_nocode=$( - cd "$GHR" && source "$STACK" 2>/dev/null + cd "$GHR" || exit 1 + # shellcheck disable=SC1090 # STACK path is dynamic by design + source "$STACK" 2>/dev/null set +e export PATH="$GHR/bin:$PATH" GH_STUB_BODY='{"message":"Not Found"}' GH_STUB_NOCODE=1 gh_release_fetch p2pool-starter-stack/pithead From 6ceda96dc8e7cdb17ade26a80548d40bc79edf05 Mon Sep 17 00:00:00 2001 From: Vijit Singh Date: Wed, 19 Aug 2026 10:16:30 -0500 Subject: [PATCH 15/47] feat(ci): watch upstream component currency, weekly, report-only (#1128) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The repo already had a pin watcher. It lived on `develop-v2`, where a `schedule:` trigger can never fire — GitHub runs schedules from the default branch — so it ran exactly zero times and the two pins it covered were never checked once. Its own header comment knew ("until the appliance tree merges there, the guard below makes every run a quiet no-op"), and the appliance tree is never going to merge to `develop`. So: on `develop`, widened to the pins #1128 is actually about, and the logic in a script rather than inline YAML so `make lint-sh` shellchecks it and tier 1 can drive it. REPORT-ONLY, deliberately. RigForge's xmrig-bump.yml opens a build-verified PR and that is right there — XMRig is a drop-in binary and the build gate proves the candidate. A Tari or monerod minor is a data migration to schedule; #1129 carries three one-time migrations and a one-way wallet-DB change. One shape, two outputs. The comparison is the whole product, not the fetch. Our pins do not spell versions the way upstream tags them — `caddy:2.11.4` vs `v2.11.4`, `minotari_node:v5.3.1-mainnet` vs `v5.6.0` — and a plain string compare reports two components stale every week for ever. A watcher that cries wolf weekly gets muted, which is exactly as useful as one that never runs. Unreachable is not current: every failed lookup is counted, the run exits non-zero, the report names what could not be checked, and the "last fully successful check" line is only written on a clean run. A watcher that has silently stopped otherwise looks identical to one with nothing to report. --- .github/workflows/pin-watch.yml | 72 +++++++++++ scripts/pin-watch.sh | 208 ++++++++++++++++++++++++++++++++ tests/stack/run.sh | 10 ++ 3 files changed, 290 insertions(+) create mode 100644 .github/workflows/pin-watch.yml create mode 100755 scripts/pin-watch.sh diff --git a/.github/workflows/pin-watch.yml b/.github/workflows/pin-watch.yml new file mode 100644 index 00000000..7ca23f43 --- /dev/null +++ b/.github/workflows/pin-watch.yml @@ -0,0 +1,72 @@ +name: Upstream pin watch + +# Weekly upstream-currency report (#1128). It REPORTS and never bumps: a Tari or monerod minor is +# a data migration to schedule, not a bump to merge (#1129 carries three one-time migrations and a +# one-way wallet-DB change). RigForge's xmrig-bump.yml opens a build-verified PR instead — same +# shape, different output, because XMRig is a drop-in binary and its build gate proves the +# candidate. +# +# ONE tracking issue, edited in place. An issue persists, is assignable and milestonable, and lets +# a human write "held until the bench is free, here's why" in a comment — which a report artifact +# nobody opens cannot do, and which is the failure mode #1128 was filed about. +# +# This file lives on the DEFAULT branch on purpose. The previous pin-watch.yml lived on +# `develop-v2`, where a `schedule:` can never fire, and so ran exactly zero times — the appliance +# pins it watched were never checked once. That is the same defect class as #1048 and #1064. +on: + schedule: + - cron: "30 6 * * 1" # Mondays 06:30 UTC, after the image sweeps + workflow_dispatch: + +# Least privilege (#282): issues.write is the job's one output — it never pushes or publishes. +permissions: + contents: read + issues: write + +jobs: + pin-watch: + name: Compare upstream component pins against their latest releases + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Build the currency report + id: report + env: + GH_TOKEN: ${{ github.token }} + run: | + set -uo pipefail + # NOT `set -e`: a non-zero exit here means "one or more lookups could not run", which is + # a result to publish, not a reason to skip publishing it. The exit code is carried to + # the last step instead, so a watcher that could not do its job says so in the artefact + # a human reads AND fails the run. + bash scripts/pin-watch.sh >report.md + rc=$? + # An empty report is itself a failure to report — never publish silence. + [ -s report.md ] || { echo "The pin watcher produced no report at all — see the run log." >report.md; rc=1; } + echo "rc=$rc" >>"$GITHUB_OUTPUT" + - name: Publish it to the one tracking issue + env: + GH_TOKEN: ${{ github.token }} + TITLE: "Upstream pin currency (weekly report)" + run: | + set -Eeuo pipefail + body=$(cat report.md) + # Exact title match over the open list, NOT `--search`: the search index lags behind + # issue creation by minutes, so a search-based dedup files a second issue on the very + # next run and then keeps both stale. + n=$(gh issue list --state open --limit 200 --json number,title \ + --jq "map(select(.title == \"$TITLE\")) | .[0].number // empty") + if [ -n "$n" ]; then + gh issue edit "$n" --body "$body" + echo "updated #$n" + else + gh issue create --title "$TITLE" --label infra --body "$body" + fi + - name: Fail the run if any lookup could not be made + if: steps.report.outputs.rc != '0' + run: | + echo "::error::one or more upstream lookups could not run — those pins are UNCHECKED, not current" + exit 1 diff --git a/scripts/pin-watch.sh b/scripts/pin-watch.sh new file mode 100755 index 00000000..54a7bc66 --- /dev/null +++ b/scripts/pin-watch.sh @@ -0,0 +1,208 @@ +#!/usr/bin/env bash +# +# Weekly upstream-currency watch (#1128). +# +# REPORTS ONLY. It never bumps a pin and never opens a PR. That is deliberate: a Tari or monerod +# minor is a data migration to schedule, not a bump to merge — #1129 carries three one-time +# migrations and a one-way wallet-DB change. RigForge's xmrig-bump.yml opens a build-verified PR +# instead, which is right there and wrong here; the two share this shape, not this output. +# +# The pins come from scripts/release.sh's pin(), which is where the release notes read them from. +# A second list is how the gap this closes opened in the first place. +# +# One question per component: is the pinned VERSION behind upstream's latest release? +# +# NOT asked here, deliberately: whether an image pinned `tag@sha256:...` still has a digest that +# corresponds to that tag. The digest is authoritative and the tag is decoration, so a bump that +# moves the tag and leaves the digest keeps running the old image while every doc says otherwise +# — but answering it needs a registry client (two different token flows for quay.io and Docker +# Hub), which is a second source type with its own failure mode. It is its own change. +# +# UNREACHABLE IS NOT CURRENT. Every failed lookup increments a counter, the run exits non-zero, +# and the report names what could not be checked. A watcher that has silently stopped otherwise +# looks exactly like a watcher with nothing to report — which is how a scheduled workflow in this +# repo ran zero times without anyone noticing. +# +# Usage: +# scripts/pin-watch.sh Print the markdown report on stdout; rc 1 if anything failed. +# scripts/pin-watch.sh --self-test Drive the comparison logic against fixtures. No network. + +set -Eeuo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +# Upstream release feed per component. Everything here is compared against +# `repos///releases/latest`, which also excludes prereleases — load-bearing, since +# tari's newest tags are v5.6.0-pre.* and proposing those onto the merge-mining leg would be worse +# than saying nothing. +# +# NOT WATCHED, on purpose, because they have no GitHub release feed: the alpine base in +# build/tor/Dockerfile, ubuntu:24.04 and python:3.11-slim. Dependabot's docker ecosystem does see +# those (it reads FROM lines), so they are covered — just not here. They are named in the report so +# their absence is a statement rather than a silence. +upstream_for() { + case "$1" in + monero) echo monero-project/monero ;; + p2pool) echo SChernykh/p2pool ;; + xmrig-proxy) echo xmrig/xmrig-proxy ;; + tari) echo tari-project/tari ;; + caddy) echo caddyserver/caddy ;; + socket-proxy) echo Tecnativa/docker-socket-proxy ;; + compose) echo docker/compose ;; + cosign) echo sigstore/cosign ;; + esac +} + +# Our pins do not spell versions the way upstream tags them, and this is the part that decides +# whether the watcher is useful or muted: `caddy:2.11.4` vs `v2.11.4` and `xmrig-proxy 6.26.0` vs +# `v6.26.0` are CURRENT, and `minotari_node:v5.3.1-mainnet` vs `v5.6.0` is stale. A plain string +# comparison calls the first two stale every week for ever, and a watcher that cries wolf weekly +# gets ignored — exactly as useless as one that never runs. +# +# Strips, in order: an image name up to the last colon, a digest suffix, a leading `v`, and a +# `-mainnet`/`-testnet` network suffix. +norm() { + local v="$1" + # Digest FIRST: `${v##*:}` cuts to the LAST colon, and a digest suffix carries one — so the + # other order turns caddy:2.11.4@sha256:abc into "abc". (Caught by the self-test below on the + # first run, which is the only reason it is not shipping that way.) + v="${v%%@*}" + v="${v##*:}" + v="${v#v}" + v="${v%-mainnet}" + v="${v%-testnet}" + printf '%s' "$v" +} + +# The digest half of a `tag@sha256:...` image pin; empty for a plain version string. +pinned_digest() { + case "$1" in + *@sha256:*) printf '%s' "${1##*@}" ;; + esac +} + +# The full image reference minus the digest, e.g. quay.io/tarilabs/minotari_node:v5.3.1-mainnet. +image_ref() { + case "$1" in + *@sha256:*) printf '%s' "${1%%@*}" ;; + esac +} + +# --- the two lookups. Both are wrapped so a failure is a COUNTED failure, never a quiet "current". + +latest_release() { # -> tag on stdout, rc 1 on any failure + local tag + tag=$(gh api "repos/$1/releases/latest" --jq .tag_name 2>/dev/null) || return 1 + # Third-party input. A tag that is not shaped like a version must not be compared, printed into + # an issue body, or otherwise trusted. + printf '%s' "$tag" | grep -qE '^v?[0-9]' || return 1 + printf '%s' "$tag" +} + + +# --- self-test ----------------------------------------------------------------------------------- +# The comparison logic is the whole product here; the two lookups are one `gh api` and one +# `docker` call each. Drives norm() over the real pin spellings and the failure paths over stubs. +if [ "${1:-}" = "--self-test" ]; then + st_fail=0 + st() { #