From 13e37f80b58ac8aea1cdac8f6d30ac9e9625f70f Mon Sep 17 00:00:00 2001 From: "Shane B." Date: Sun, 23 Aug 2026 11:25:16 +0200 Subject: [PATCH 1/2] Take the FindMy.py fix for the peer id the shares are fetched for Pin moves 337381de -> 4f940158, which carries jamorenom's fix for #140 and the three things the review of it turned up. `RecoveredPeer.peer_id` returned the escrow record's label suffix. Cuttlefish addresses peers by the SHA256-prefixed hash on the sealed bottle, and on some accounts those are different strings - so `FetchRecoverableTlkShares` asked for a peer that does not exist, and Apple answered with every view's key set and no shares at all. The export died at `zone_keys()` reporting that no keychain keys are held, which reads as an account or permissions problem and is not one. Nothing in this repo changes but the revision string. `exporter/` never touches `peer_id` - it asks for `recovery_options` and `zone_keys` and takes what it is given - which is why the whole failure arrived here as an empty result. What came in beyond the reported fix: - the peer id now comes from the trust circle, with the bottle's ids as the fallback for a peer the circle no longer lists - `join_trust_circle` refuses a sponsor the circle does not contain. That was the third use of this property and it is the one that writes: a voucher naming an unknown sponsor is signed, sent and permanent - an end-to-end harness over a synthetic account in **both** shapes - label suffix agreeing with the circle id, and diverging. Reintroducing #140 fails the divergent variant and passes the agreeing one, which is what happened in the world That last part is what makes this safe to take. This account is an agreeing one, so it took the branch the fallback still takes, and nothing offline here could have caught the bug in the first place. 508 exporter tests pass against the new pin. **The fix itself is unverified from here**: it needs an account whose escrow label diverges, and neither this side nor the FindMy.py side has one. jamorenom reports 0/22 shares before and 21/21 after on two unrelated accounts, and has offered to re-run; that is the only evidence it works, and it is theirs. Co-Authored-By: jamorenom Co-Authored-By: Claude Opus 5 --- app/src/test/python/requirements.txt | 2 +- python/pyproject.toml | 2 +- python/uv.lock | 32 ++++++++++++++-------------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/src/test/python/requirements.txt b/app/src/test/python/requirements.txt index 6464bc3a..b96abbc8 100644 --- a/app/src/test/python/requirements.txt +++ b/app/src/test/python/requirements.txt @@ -9,7 +9,7 @@ # `FindMy==0.9.8` here for as long as the app built the fork, so every bridge test # ran against a library the app does not ship - which is not a small difference: # the fork's Anisette providers take `serial=` and PyPI's do not. -git+https://github.com/parawanderer/FindMy.py@337381dedf4662d730752e727d896f964feae1f8 +git+https://github.com/parawanderer/FindMy.py@4f940158c437b17ae61a8e90af084d658750629c NSKeyedUnArchiver==1.5 PyYAML==6.0.2 diff --git a/python/pyproject.toml b/python/pyproject.toml index c8d45e89..fbc09a45 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -69,7 +69,7 @@ constraint-dependencies = [ ] [tool.uv.sources] -FindMy = { git = "https://github.com/parawanderer/FindMy.py", rev = "337381dedf4662d730752e727d896f964feae1f8" } +FindMy = { git = "https://github.com/parawanderer/FindMy.py", rev = "4f940158c437b17ae61a8e90af084d658750629c" } [dependency-groups] # Only the release build installs this, with `uv sync --no-default-groups --group build`. It is diff --git a/python/uv.lock b/python/uv.lock index 26b0ac17..8cde8c92 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -529,8 +529,8 @@ resolution-markers = [ "platform_machine == 'x86_64' and sys_platform == 'darwin'", ] dependencies = [ - { name = "cffi", marker = "platform_machine == 'x86_64' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/12/45/870e7f4bef50e5f53b9f51d4428aee5290eedf58ba443f16b1ebb7ab8e66/cryptography-48.0.1.tar.gz", hash = "sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a", size = 832989, upload-time = "2026-06-09T22:32:31.8Z" } wheels = [ @@ -547,8 +547,8 @@ resolution-markers = [ "platform_machine != 'x86_64' or sys_platform != 'darwin'", ] dependencies = [ - { name = "cffi", marker = "(platform_machine != 'x86_64' and platform_python_implementation != 'PyPy') or (platform_python_implementation != 'PyPy' and sys_platform != 'darwin')" }, - { name = "typing-extensions", marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } wheels = [ @@ -649,7 +649,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -659,7 +659,7 @@ wheels = [ [[package]] name = "findmy" version = "0.10.1" -source = { git = "https://github.com/parawanderer/FindMy.py?rev=337381dedf4662d730752e727d896f964feae1f8#337381dedf4662d730752e727d896f964feae1f8" } +source = { git = "https://github.com/parawanderer/FindMy.py?rev=4f940158c437b17ae61a8e90af084d658750629c#4f940158c437b17ae61a8e90af084d658750629c" } dependencies = [ { name = "aiohttp" }, { name = "anisette" }, @@ -1032,7 +1032,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "findmy", git = "https://github.com/parawanderer/FindMy.py?rev=337381dedf4662d730752e727d896f964feae1f8" }, + { name = "findmy", git = "https://github.com/parawanderer/FindMy.py?rev=4f940158c437b17ae61a8e90af084d658750629c" }, { name = "pycryptodome", specifier = "==3.22.0" }, { name = "pyyaml", specifier = "==6.0.2" }, { name = "pyzipper", specifier = "==0.4.0" }, @@ -1717,7 +1717,7 @@ name = "winrt-runtime" version = "3.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/16/dd/acdd527c1d890c8f852cc2af644aa6c160974e66631289420aa871b05e65/winrt_runtime-3.2.1.tar.gz", hash = "sha256:c8dca19e12b234ae6c3dadf1a4d0761b51e708457492c13beb666556958801ea", size = 21721, upload-time = "2025-06-06T14:40:27.593Z" } wheels = [ @@ -1743,7 +1743,7 @@ name = "winrt-windows-devices-bluetooth" version = "3.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "winrt-runtime", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "winrt-runtime" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b2/a0/1c8a0c469abba7112265c6cb52f0090d08a67c103639aee71fc690e614b8/winrt_windows_devices_bluetooth-3.2.1.tar.gz", hash = "sha256:db496d2d92742006d5a052468fc355bf7bb49e795341d695c374746113d74505", size = 23732, upload-time = "2025-06-06T14:41:20.489Z" } wheels = [ @@ -1769,7 +1769,7 @@ name = "winrt-windows-devices-bluetooth-advertisement" version = "3.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "winrt-runtime", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "winrt-runtime" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/fc/7ffe66ca4109b9e994b27c00f3d2d506e6e549e268791f755287ad9106d8/winrt_windows_devices_bluetooth_advertisement-3.2.1.tar.gz", hash = "sha256:0223852a7b7fa5c8dea3c6a93473bd783df4439b1ed938d9871f947933e574cc", size = 16906, upload-time = "2025-06-06T14:41:21.448Z" } wheels = [ @@ -1795,7 +1795,7 @@ name = "winrt-windows-devices-bluetooth-genericattributeprofile" version = "3.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "winrt-runtime", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "winrt-runtime" }, ] sdist = { url = "https://files.pythonhosted.org/packages/44/21/aeeddc0eccdfbd25e543360b5cc093233e2eab3cdfb53ad3cabae1b5d04d/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1.tar.gz", hash = "sha256:cdf6ddc375e9150d040aca67f5a17c41ceaf13a63f3668f96608bc1d045dde71", size = 38896, upload-time = "2025-06-06T14:41:22.687Z" } wheels = [ @@ -1821,7 +1821,7 @@ name = "winrt-windows-devices-enumeration" version = "3.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "winrt-runtime", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "winrt-runtime" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9e/dd/75835bfbd063dffa152109727dedbd80f6e92ea284855f7855d48cdf31c9/winrt_windows_devices_enumeration-3.2.1.tar.gz", hash = "sha256:df316899e39bfc0ffc1f3cb0f5ee54d04e1d167fbbcc1484d2d5121449a935cf", size = 23538, upload-time = "2025-06-06T14:41:26.787Z" } wheels = [ @@ -1847,7 +1847,7 @@ name = "winrt-windows-devices-radios" version = "3.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "winrt-runtime", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "winrt-runtime" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5e/02/9704ea359ad8b0d6faa1011f98fb477e8fb6eac5201f39d19e73c2407e7b/winrt_windows_devices_radios-3.2.1.tar.gz", hash = "sha256:4dc9b9d1501846049eb79428d64ec698d6476c27a357999b78a8331072e18a0b", size = 5908, upload-time = "2025-06-06T14:41:44.868Z" } wheels = [ @@ -1873,7 +1873,7 @@ name = "winrt-windows-foundation" version = "3.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "winrt-runtime", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "winrt-runtime" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0c/55/098ce7ea0679efcc1298b269c48768f010b6c68f90c588f654ec874c8a74/winrt_windows_foundation-3.2.1.tar.gz", hash = "sha256:ad2f1fcaa6c34672df45527d7c533731fdf65b67c4638c2b4aca949f6eec0656", size = 30485, upload-time = "2025-06-06T14:41:53.344Z" } wheels = [ @@ -1899,7 +1899,7 @@ name = "winrt-windows-foundation-collections" version = "3.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "winrt-runtime", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "winrt-runtime" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/62/d21e3f1eeb8d47077887bbf0c3882c49277a84d8f98f7c12bda64d498a07/winrt_windows_foundation_collections-3.2.1.tar.gz", hash = "sha256:0eff1ad0d8d763ad17e9e7bbd0c26a62b27215016393c05b09b046d6503ae6d5", size = 16043, upload-time = "2025-06-06T14:41:53.983Z" } wheels = [ @@ -1925,7 +1925,7 @@ name = "winrt-windows-storage-streams" version = "3.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "winrt-runtime", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "winrt-runtime" }, ] sdist = { url = "https://files.pythonhosted.org/packages/00/50/f4488b07281566e3850fcae1021f0285c9653992f60a915e15567047db63/winrt_windows_storage_streams-3.2.1.tar.gz", hash = "sha256:476f522722751eb0b571bc7802d85a82a3cae8b1cce66061e6e758f525e7b80f", size = 34335, upload-time = "2025-06-06T14:43:23.905Z" } wheels = [ From 905815f20ce0674cca08e6d83a671aae3b98abb2 Mon Sep 17 00:00:00 2001 From: "Shane B." Date: Sun, 23 Aug 2026 11:37:13 +0200 Subject: [PATCH 2/2] Move the fourth pin, and write down that there are four The bump missed `app/build.gradle.kts`, so the APK would have shipped FindMy.py 337381de while the exporter and the bridge tests ran 4f940158. The two guard tests in `test_main.py` caught it, which is the whole reason they exist. **It presents as the APK build failing.** The bridge tests run in an earlier step of the same job, so a missed pin skips the APK steps and the actual assertion is several steps up the log, saying nothing about Gradle. Rule 14 lists all four locations, which of them is generated, the two tests that enforce it, and how to run them locally in under a second. There is no single manifest to consult - `opentagviewer_export` runs under both interpreters, so every dependency is declared once per consumer - and nothing about the layout says so. Co-Authored-By: Claude Opus 5 --- AGENTS.md | 45 ++++++++++++++++++++++++++++++++++++++++++++ app/build.gradle.kts | 2 +- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index fc60f824..57f2efe4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -288,6 +288,51 @@ Two things follow: repository and the whole Python bridge are instrumented. Rule 12 is about what genuinely needs a device; this rule is about not sending it things that do not. +### 14. A Python dependency is pinned in four places, and they all move together + +There is no single manifest. `opentagviewer_export` is shared code that runs under **both** +interpreters — Chaquopy packages it into the APK, and the desktop exporter runs the same files +from source — so every Python dependency is declared once per consumer: + +| Where | What it feeds | How to change it | +| --- | --- | --- | +| `app/build.gradle.kts` — the `install(…)` lines in the Chaquopy block | **what ships in the APK** | by hand | +| `app/src/test/python/requirements.txt` | the Chaquopy bridge tests | by hand | +| `python/pyproject.toml` | the desktop exporter and its tests | by hand | +| `python/uv.lock` | what `uv sync --frozen` actually installs | `cd python && uv lock` — never by hand | + +**Miss one and the two halves ship different libraries.** That is not version skew: FindMy.py is +pinned by commit off a fork, and its API moves between commits (rule 3), so one consumer gets +methods the other does not. It has happened — `requirements.txt` said `FindMy==0.9.8` from PyPI +while the app built the fork, and both lines were individually true for months. + +Two tests in `app/src/test/python/test_main.py` enforce it, and they are worth knowing by name +because they fail in a place that looks unrelated: + +- `test_the_whole_repository_pins_one_findmy` +- `test_pinned_versions_match_the_app_build` — every install, including the `git+…` ones, which + is the shape that was invisible to it before + +They run in the **Chaquopy bridge tests** step of `build-debug.yml`, which is *before* the APK is +built. So a missed pin presents as **"the APK build failed"** with the APK steps merely skipped, +and the actual assertion is several steps up the log. Read the step list before believing the +build broke: + +```bash +gh api repos/parawanderer/OpenTagViewer/actions/jobs/ \ + --jq '.steps[] | "\(.conclusion // .status)\t\(.name)"' +``` + +Run them before pushing — they need no device and take under a second: + +```bash +python -m venv .venv && .venv/bin/pip install -r app/src/test/python/requirements.txt +.venv/bin/python -m pytest app/src/test/python -q +``` + +The same applies to every other shared dependency, not just FindMy.py: `PyYAML` is pinned in +`app/build.gradle.kts` and `python/pyproject.toml` for exactly this reason. + --- ## Building and testing diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ffb7bf4a..39ea4d84 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -368,7 +368,7 @@ chaquopy { // wheel for desktop platforms and a pure-Python `py3-none-any` one as well. // There is no Android wheel, so pip falls back to the pure-Python build - which // is correct but markedly slower. The messages here are small enough not to care. - install("git+https://github.com/parawanderer/FindMy.py@337381dedf4662d730752e727d896f964feae1f8") + install("git+https://github.com/parawanderer/FindMy.py@4f940158c437b17ae61a8e90af084d658750629c") install("NSKeyedUnArchiver==1.5")