diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 464d8f8a..0328e26a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -360,16 +360,18 @@ jobs: - name: Build PyPI distribution working-directory: pkg/pypi run: | - # Pin to specific versions (typosquatting / supply-chain mitigation). - # OSSF scorecard prefers --require-hashes, but that needs full - # transitive deps; version pinning is the practical compromise. + # Every byte of the publish toolchain is bound by hash: version pins + # alone still let a compromised index serve different content, and a + # bare `pip install` resolves the whole transitive graph at run time + # (OSSF Scorecard pinned-dependencies). requirements-publish.txt is + # generated on a linux/amd64 python:3.12 image — see its header for + # the exact regeneration command. # - # twine >= 7 is required: `python -m build` resolves the UNPINNED - # hatchling backend fresh in its isolated env, and current hatchling - # emits Metadata-Version 2.5, which twine 6.2.0 rejects as invalid - # ("'2.5' is not a valid metadata version" — the v0.10.1 failure). - # Verified locally: identical artifacts, 6.2.0 rejects / 7.0.0 passes. - python -m pip install --upgrade 'build==1.3.0' 'twine==7.0.0' + # twine must stay >= 7 and hatchling is pinned in pyproject.toml: + # `python -m build` resolves its backend fresh in an isolated env, and + # a hatchling emitting Metadata-Version 2.5 against twine 6.2.0 is + # what broke the v0.10.1 publish. + python -m pip install --require-hashes -r requirements-publish.txt python -m build twine check dist/* @@ -439,6 +441,16 @@ jobs: permissions: contents: write steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + # Legacy ui-* aliases so 0.9.x updaters stop 404ing (#1538). Runs after + # verify: same bytes as archives the VirusTotal gate already cleared, and + # publishing earlier would duplicate the scan set and provenance manifest. + - name: Publish legacy ui-* archive aliases + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: scripts/ci/publish-legacy-aliases.sh "${{ inputs.version }}" "$GITHUB_REPOSITORY" + - name: Un-draft GitHub release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 86a21806..83658c62 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,9 @@ soak-results-query-leak/ # scripts/soak-test.sh stay tracked. /soak*/ /memlab-* + +# Compiled Go wrapper binary. `go build` in pkg/go drops the executable beside +# its sources; one was committed by accident (8.8 MB ELF, flagged by OSSF +# Scorecard as a binary artifact in source). Source only, never the build. +/pkg/go/codebase-memory-mcp +/pkg/go/codebase-memory-mcp.exe diff --git a/SECURITY.md b/SECURITY.md index b7d08133..279dec00 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -288,8 +288,8 @@ for Linux ELF binaries, so it is not a complete answer either. | Version | Supported | |---------|-----------| -| Latest `0.9.x` | Yes — security fixes land in the newest release | -| < 0.9 | No — please upgrade to the latest release | +| Latest `0.10.x` | Yes — security fixes land in the newest release | +| < 0.10 | No — please upgrade to the latest release | Only the latest release is supported. Security fixes are shipped in a new patched release rather than backported to older versions; upgrading to the diff --git a/graph-ui/package-lock.json b/graph-ui/package-lock.json index 8c6ae46c..7768b039 100644 --- a/graph-ui/package-lock.json +++ b/graph-ui/package-lock.json @@ -5134,9 +5134,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { diff --git a/graph-ui/package.json b/graph-ui/package.json index c2667999..b1aa3998 100644 --- a/graph-ui/package.json +++ b/graph-ui/package.json @@ -41,6 +41,7 @@ }, "overrides": { "form-data": ">=4.0.6", - "@babel/core": ">=7.29.6" + "@babel/core": ">=7.29.6", + "nanoid": "^3.3.17" } } diff --git a/install.sh b/install.sh index 3b99e735..aa48ac27 100755 --- a/install.sh +++ b/install.sh @@ -237,8 +237,13 @@ fi # macOS: fix signing if [ "$OS" = "darwin" ]; then echo "Fixing macOS code signing..." - xattr -d com.apple.quarantine "$DLBIN" 2>/dev/null || true - codesign --sign - --force "$DLBIN" 2>/dev/null || true + # A curl-downloaded archive usually carries no quarantine attribute at all, + # and xattr then prints "No such xattr: com.apple.quarantine" on stderr. + # That harmless line was read as the cause of an unrelated install failure + # and became a bug report's title (#1537) — silence it; nothing here is an + # error worth showing. + xattr -d com.apple.quarantine "$DLBIN" >/dev/null 2>&1 || true + codesign --sign - --force "$DLBIN" >/dev/null 2>&1 || true fi # Verify the candidate before it requests account-wide maintenance. The diff --git a/pkg/go/codebase-memory-mcp b/pkg/go/codebase-memory-mcp deleted file mode 100644 index f8ea349b..00000000 Binary files a/pkg/go/codebase-memory-mcp and /dev/null differ diff --git a/pkg/npm/README.md b/pkg/npm/README.md index c64df22b..e6efc83a 100644 --- a/pkg/npm/README.md +++ b/pkg/npm/README.md @@ -15,11 +15,7 @@ High-quality parsing through [tree-sitter](https://tree-sitter.github.io/tree-si npm install -g codebase-memory-mcp ``` -The standard runtime set for your platform is downloaded automatically at install time. To select the UI runtime set and its verified frontend pack, set `CBM_VARIANT=ui` while installing and when invoking the wrapper: - -```bash -CBM_VARIANT=ui npm install -g codebase-memory-mcp -``` +The runtime set for your platform is downloaded automatically at install time. There is one composition per platform and the graph UI is always included — the former `CBM_VARIANT=ui` opt-in is obsolete. Then configure your coding agents: diff --git a/pkg/npm/install.js b/pkg/npm/install.js index 2c3ea9b6..9c86e933 100644 --- a/pkg/npm/install.js +++ b/pkg/npm/install.js @@ -580,14 +580,13 @@ async function main() { // dynamically links glibc 2.38+ and fails on older distros. macOS/Windows // have no such variant. Keep in sync with install.sh / pypi _cli.py / cli.c. const variant = platform === 'linux' ? '-portable' : ''; - // Opt into the UI build (embedded graph visualization) with CBM_VARIANT=ui. - // Default is the standard (headless) build. Mirrors install.sh --ui. - const ui = (process.env.CBM_VARIANT || '').toLowerCase() === 'ui' ? 'ui-' : ''; - const archive = `codebase-memory-mcp-${ui}${platform}-${arch}${variant}.${ext}`; + // No UI/standard split since v0.10.0: one archive per platform, graph UI + // always embedded. The old CBM_VARIANT=ui opt-in pointed at ui-* archives + // that no longer exist, so honoring it could only 404 (#1538). + const archive = `codebase-memory-mcp-${platform}-${arch}${variant}.${ext}`; const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${archive}`; - const uiLabel = ui ? '(ui) ' : ''; - process.stdout.write(`codebase-memory-mcp: downloading v${VERSION} ${uiLabel}for ${platform}/${arch}...\n`); + process.stdout.write(`codebase-memory-mcp: downloading v${VERSION} for ${platform}/${arch}...\n`); const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cbm-install-')); const tmpArchive = path.join(tmpDir, `cbm.${ext}`); diff --git a/pkg/pypi/README.md b/pkg/pypi/README.md index f75af871..a2047d87 100644 --- a/pkg/pypi/README.md +++ b/pkg/pypi/README.md @@ -4,7 +4,7 @@ mcp-name: io.github.DeusData/codebase-memory-mcp **Fast code intelligence engine for AI coding agents.** Indexes an average repository in milliseconds, the Linux kernel (28M LOC) in 3 minutes. Answers structural queries in under 1ms. -This Python wrapper downloads the selected `codebase-memory-mcp` runtime set from [GitHub Releases](https://github.com/DeusData/codebase-memory-mcp/releases) on first run and verifies it before publishing it in your OS cache directory. The standard set contains the native executable and authenticated integration asset; `CBM_VARIANT=ui` additionally selects the content-addressed UI pack. +This Python wrapper downloads the selected `codebase-memory-mcp` runtime set from [GitHub Releases](https://github.com/DeusData/codebase-memory-mcp/releases) on first run and verifies it before publishing it in your OS cache directory. The set contains the native executable and authenticated integration asset, with the graph UI always embedded. ## Installation @@ -14,7 +14,7 @@ pip install codebase-memory-mcp pipx install codebase-memory-mcp ``` -To use the UI variant, set `CBM_VARIANT=ui` when invoking the wrapper (and consistently for any package-managed update or reinstall). +There is one composition per platform: the graph UI ships in every build, so no variant selection is needed. ## Usage diff --git a/pkg/pypi/pyproject.toml b/pkg/pypi/pyproject.toml index 18807810..38ad6ec6 100644 --- a/pkg/pypi/pyproject.toml +++ b/pkg/pypi/pyproject.toml @@ -1,5 +1,9 @@ [build-system] -requires = ["hatchling"] +# Pinned: `python -m build` resolves the backend fresh in an isolated env, so an +# unpinned hatchling silently follows upstream. A hatchling release that began +# emitting Metadata-Version 2.5 broke the v0.10.1 PyPI publish with pinned +# twine 6.2.0 rejecting it — the whole toolchain must be pinned, not half of it. +requires = ["hatchling==1.28.0"] build-backend = "hatchling.build" [project] diff --git a/pkg/pypi/requirements-publish.txt b/pkg/pypi/requirements-publish.txt new file mode 100644 index 00000000..6204a7d3 --- /dev/null +++ b/pkg/pypi/requirements-publish.txt @@ -0,0 +1,359 @@ +# Hash-pinned build toolchain for the PyPI publish step (OSSF Scorecard +# pinned-dependencies: a pip command that resolves anything at run time is a +# supply-chain hole, and version pins alone do not bind the artifact bytes). +# +# Regenerate on a linux/amd64 python:3.12 image so the hashes match the wheels +# ubuntu-latest actually resolves: +# +# docker run --rm --platform linux/amd64 python:3.12-slim sh -c " +# pip install -q pip-tools==7.5.1 +# printf 'build==1.3.0\ntwine==7.0.0\n' > /tmp/req.in +# pip-compile --generate-hashes --allow-unsafe --quiet --output-file=- /tmp/req.in" +# +# twine must stay >= 7: hatchling emits Metadata-Version 2.5, which twine 6.2.0 +# rejects — that is what broke the v0.10.1 publish. +build==1.3.0 \ + --hash=sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397 \ + --hash=sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4 +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ + --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 +cffi==2.1.1 \ + --hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \ + --hash=sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66 \ + --hash=sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2 \ + --hash=sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0 \ + --hash=sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6 \ + --hash=sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971 \ + --hash=sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c \ + --hash=sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d \ + --hash=sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9 \ + --hash=sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517 \ + --hash=sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735 \ + --hash=sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80 \ + --hash=sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f \ + --hash=sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1 \ + --hash=sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29 \ + --hash=sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8 \ + --hash=sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c \ + --hash=sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e \ + --hash=sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48 \ + --hash=sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813 \ + --hash=sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac \ + --hash=sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632 \ + --hash=sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6 \ + --hash=sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1 \ + --hash=sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659 \ + --hash=sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688 \ + --hash=sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004 \ + --hash=sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0 \ + --hash=sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062 \ + --hash=sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779 \ + --hash=sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94 \ + --hash=sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50 \ + --hash=sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab \ + --hash=sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac \ + --hash=sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6 \ + --hash=sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676 \ + --hash=sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1 \ + --hash=sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9 \ + --hash=sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf \ + --hash=sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13 \ + --hash=sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e \ + --hash=sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e \ + --hash=sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973 \ + --hash=sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527 \ + --hash=sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72 \ + --hash=sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890 \ + --hash=sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c \ + --hash=sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990 \ + --hash=sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd \ + --hash=sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9 \ + --hash=sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94 \ + --hash=sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3 \ + --hash=sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80 \ + --hash=sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41 \ + --hash=sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5 \ + --hash=sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c \ + --hash=sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a \ + --hash=sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4 \ + --hash=sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e \ + --hash=sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6 \ + --hash=sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98 \ + --hash=sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b \ + --hash=sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1 \ + --hash=sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03 \ + --hash=sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af \ + --hash=sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231 \ + --hash=sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2 \ + --hash=sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3 \ + --hash=sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836 \ + --hash=sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5 \ + --hash=sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399 \ + --hash=sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96 \ + --hash=sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e \ + --hash=sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be \ + --hash=sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf \ + --hash=sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc \ + --hash=sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455 \ + --hash=sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0 \ + --hash=sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12 \ + --hash=sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b \ + --hash=sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7 \ + --hash=sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692 \ + --hash=sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54 \ + --hash=sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3 \ + --hash=sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b \ + --hash=sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be \ + --hash=sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d \ + --hash=sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358 \ + --hash=sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a \ + --hash=sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7 \ + --hash=sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc \ + --hash=sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960 \ + --hash=sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125 \ + --hash=sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb \ + --hash=sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a \ + --hash=sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa \ + --hash=sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf \ + --hash=sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3 \ + --hash=sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4 \ + --hash=sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264 +charset-normalizer==3.4.9 \ + --hash=sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380 \ + --hash=sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62 \ + --hash=sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c \ + --hash=sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226 \ + --hash=sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5 \ + --hash=sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833 \ + --hash=sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b \ + --hash=sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99 \ + --hash=sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501 \ + --hash=sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec \ + --hash=sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698 \ + --hash=sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4 \ + --hash=sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a \ + --hash=sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3 \ + --hash=sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2 \ + --hash=sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a \ + --hash=sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e \ + --hash=sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4 \ + --hash=sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419 \ + --hash=sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84 \ + --hash=sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da \ + --hash=sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519 \ + --hash=sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe \ + --hash=sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381 \ + --hash=sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29 \ + --hash=sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614 \ + --hash=sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0 \ + --hash=sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe \ + --hash=sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29 \ + --hash=sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0 \ + --hash=sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917 \ + --hash=sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9 \ + --hash=sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32 \ + --hash=sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94 \ + --hash=sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63 \ + --hash=sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd \ + --hash=sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198 \ + --hash=sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde \ + --hash=sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012 \ + --hash=sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1 \ + --hash=sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15 \ + --hash=sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b \ + --hash=sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993 \ + --hash=sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4 \ + --hash=sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5 \ + --hash=sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8 \ + --hash=sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35 \ + --hash=sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642 \ + --hash=sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2 \ + --hash=sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d \ + --hash=sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9 \ + --hash=sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c \ + --hash=sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33 \ + --hash=sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db \ + --hash=sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf \ + --hash=sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9 \ + --hash=sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee \ + --hash=sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84 \ + --hash=sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44 \ + --hash=sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f \ + --hash=sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9 \ + --hash=sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9 \ + --hash=sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177 \ + --hash=sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8 \ + --hash=sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b \ + --hash=sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39 \ + --hash=sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41 \ + --hash=sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0 \ + --hash=sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616 \ + --hash=sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d \ + --hash=sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba \ + --hash=sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2 \ + --hash=sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b \ + --hash=sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9 \ + --hash=sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b \ + --hash=sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209 \ + --hash=sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48 \ + --hash=sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046 \ + --hash=sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632 \ + --hash=sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a \ + --hash=sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2 \ + --hash=sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1 \ + --hash=sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b \ + --hash=sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990 \ + --hash=sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5 \ + --hash=sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b \ + --hash=sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9 \ + --hash=sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534 \ + --hash=sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81 \ + --hash=sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a \ + --hash=sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d \ + --hash=sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf \ + --hash=sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115 +cryptography==50.0.0 \ + --hash=sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03 \ + --hash=sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7 \ + --hash=sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437 \ + --hash=sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987 \ + --hash=sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025 \ + --hash=sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037 \ + --hash=sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269 \ + --hash=sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105 \ + --hash=sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc \ + --hash=sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95 \ + --hash=sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b \ + --hash=sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47 \ + --hash=sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c \ + --hash=sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41 \ + --hash=sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c \ + --hash=sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d \ + --hash=sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7 \ + --hash=sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c \ + --hash=sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708 \ + --hash=sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef \ + --hash=sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f \ + --hash=sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f \ + --hash=sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a \ + --hash=sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f \ + --hash=sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a \ + --hash=sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a \ + --hash=sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e \ + --hash=sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3 \ + --hash=sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d \ + --hash=sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3 \ + --hash=sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f \ + --hash=sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae \ + --hash=sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30 \ + --hash=sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9 \ + --hash=sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9 \ + --hash=sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07 \ + --hash=sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba \ + --hash=sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3 \ + --hash=sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f \ + --hash=sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533 \ + --hash=sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5 \ + --hash=sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11 \ + --hash=sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9 \ + --hash=sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f \ + --hash=sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169 \ + --hash=sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645 +docutils==0.23 \ + --hash=sha256:25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea \ + --hash=sha256:746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e +id==1.6.1 \ + --hash=sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069 \ + --hash=sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 +jaraco-classes==3.4.0 \ + --hash=sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd \ + --hash=sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790 +jaraco-context==6.1.2 \ + --hash=sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535 \ + --hash=sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3 +jaraco-functools==4.6.0 \ + --hash=sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280 \ + --hash=sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30 +jeepney==0.9.0 \ + --hash=sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683 \ + --hash=sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732 +keyring==25.7.0 \ + --hash=sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f \ + --hash=sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b +markdown-it-py==4.2.0 \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a +mdurl==0.1.2 \ + --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ + --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba +more-itertools==11.1.0 \ + --hash=sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d \ + --hash=sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192 +nh3==0.3.6 \ + --hash=sha256:082675ff87b9385ec430ffe6d5847ba7456cc39b73720cd4add472f9f4cffd56 \ + --hash=sha256:2411e8c3cee81a1ddd62c2a5d50585c28aa5566d373ad1db92536b95ddb24ef2 \ + --hash=sha256:25c733bee928530556b1db0ea46c52cf5aa686146e38e60a6fc7cb801ef91cec \ + --hash=sha256:2f90d9a0cfdbee218994fdaaeeb5a0fde62d08f35e4eef0378ec1e2200172fd0 \ + --hash=sha256:34d2b0d934156b87ee114f599a3ba9b8b9e17b5d79652ba3a13fa50903de965e \ + --hash=sha256:36d06341bd501240d320f5942481ed5e6846136b666e1ba4faf802b78ebc875f \ + --hash=sha256:43bc1ed3fa0716295fabee29ba42b2667e4a51d140b0a68e092170a765474fa6 \ + --hash=sha256:44673b27010051ab5a5e438a86ec31bbda61d4a77d7e900af6b7be3037c1abae \ + --hash=sha256:455469a29951edc92bc48b47ac2281c3f2609e6c4f6a047056449f8c2c23facf \ + --hash=sha256:4713502748f564fee0633b37b3403783ce0a3af3a3d148ad91025a5bdadb7bc6 \ + --hash=sha256:5276ef17bdba9ad8040575c74072008b13aae429436e9d0429e718bb5f90f4da \ + --hash=sha256:597a8e843bea00b2eb5520658dc24a9bb032e7fc9e7c2c0c4cd29420220c9796 \ + --hash=sha256:69bbb92865a693d909db3a700d3c01537533844d0948c1e9323561ce06ecda41 \ + --hash=sha256:69f365963f63a1e9bff53bdbb3c542c7c2efed3e163c9d5d83a772a2ac468c21 \ + --hash=sha256:82ca5bf427ad1b216b65ede1a2e2d87dc49bec417ceba0f297213107d3cd9d78 \ + --hash=sha256:889932a97fb4abb6f95fef1914c0d269ebfb60011e67121c1163059b9449dbb4 \ + --hash=sha256:905f877dc66dd7aea4a76e54bcb26acb5ff8216f720c0017ccf63e0e6035698e \ + --hash=sha256:a43ebd7543555c3ac1bc353023d0794e75cb76f6f18f19c32e95441496c0cc25 \ + --hash=sha256:d14bf7982e7a77c0c775634c29c07ce08b38a046df73e1c1f139b3e82f18a38e \ + --hash=sha256:e196fa70c2ff2eb4de7d3df3108f8f358c1d69dff20d45b11f20a5aa227ffb6d \ + --hash=sha256:e1b160831c9cdb06a6c79c2f9cdb11386602938f9af260d1c457a85add4f6f69 \ + --hash=sha256:e6b7beece07525dc6e6b0fc2f104442de2ba328360ad00e50cbe2e1fd620447d \ + --hash=sha256:edb2b4a1a27523e6cc7c417f8d21ce3d005243548b93e56b762b66b0c7f589f9 \ + --hash=sha256:f2f14b7ae1fca99c4a66c981aac3974e7fbc1ca30a12673d223ae1df76680917 \ + --hash=sha256:f338ac7d594c067679f1e99b4f5ec3906842979560f9d8f15d6bdfa39a353b10 \ + --hash=sha256:f3736c9dd3d1856f80cd031715b84ca75cda2bbb1ac802c3da26bfce590838d7 \ + --hash=sha256:f5ed5fe84aee7f39db95c214a7421bf0499fbf500fec6d86a4e29bfc37971438 +packaging==26.3 \ + --hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \ + --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c +pycparser==3.0 \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 +pyproject-hooks==1.2.0 \ + --hash=sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8 \ + --hash=sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913 +readme-renderer==45.0 \ + --hash=sha256:030a8fac74904f8fba11ad1bb6964e3f76e896dc7e5e71f16af190c9056696d1 \ + --hash=sha256:3385ed220117104a2bceb4a9dac8c5fdf6d1f96890d7ea2a9c7174fd5c84091f +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed +requests-toolbelt==1.0.0 \ + --hash=sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6 \ + --hash=sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06 +rfc3986==2.0.0 \ + --hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \ + --hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c +rich==15.0.0 \ + --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \ + --hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36 +secretstorage==3.5.0 \ + --hash=sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137 \ + --hash=sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be +twine==7.0.0 \ + --hash=sha256:85cdb29c518efef867360ae4acd4b0dfd61c8654a22fca08e6f8539f05022177 \ + --hash=sha256:b854164df26db268af05f49aa5c0344b10e27a494343ff05b1e0bad3b135f5a7 +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 diff --git a/scripts/ci/publish-legacy-aliases.sh b/scripts/ci/publish-legacy-aliases.sh new file mode 100755 index 00000000..07f0b398 --- /dev/null +++ b/scripts/ci/publish-legacy-aliases.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Publish byte-identical ui-*-named copies of the release archives. +# +# Every codebase-memory-mcp 0.9.x binary in the field asks for +# codebase-memory-mcp-ui-. when its (now-removed) variant chooser +# is answered "ui". v0.10.0 consolidated to a single archive per platform with +# the UI always embedded, so that asset stopped existing and those updaters 404 +# with no path forward but a manual reinstall (#1538, discussion #1526). +# Publishing the same bytes under the legacy names makes already-released +# updaters work again with no action from the user. +# +# Runs AFTER the VirusTotal gate: the aliases are copies of archives the gate +# already cleared, and uploading them earlier would duplicate every object in +# the scan set and in the provenance manifest. They are likewise absent from +# checksums.txt, which covers the canonical names current installers request. +# +# Usage: publish-legacy-aliases.sh +set -euo pipefail + +VERSION="${1:?usage: publish-legacy-aliases.sh }" +REPOSITORY="${2:?usage: publish-legacy-aliases.sh }" + +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT + +gh release download "$VERSION" --dir "$work" --repo "$REPOSITORY" \ + --pattern 'codebase-memory-mcp-*.tar.gz' --pattern 'codebase-memory-mcp-*.zip' + +shopt -s nullglob +aliases=() +for archive in "$work"/codebase-memory-mcp-*; do + base="$(basename "$archive")" + case "$base" in + codebase-memory-mcp-ui-*) continue ;; + esac + alias_path="$work/${base/codebase-memory-mcp-/codebase-memory-mcp-ui-}" + cp -- "$archive" "$alias_path" + aliases+=("$alias_path") +done + +if [ "${#aliases[@]}" -eq 0 ]; then + echo "error: no canonical archives found to alias for $VERSION" >&2 + exit 1 +fi + +gh release upload "$VERSION" "${aliases[@]}" --clobber --repo "$REPOSITORY" +echo "published ${#aliases[@]} legacy alias asset(s) for $VERSION" diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index 90efe52d..53dc130a 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -1166,7 +1166,7 @@ if ! echo "$UNINSTALL_OUT" | grep -qi 'uninstall\|remov'; then fi echo "OK: uninstall --dry-run completed" -# 6c: update --dry-run --standard -y +# 6c: update --dry-run -y # The product binary never replaces itself on ANY platform. `update` is a # handoff: it prints the shipped install script's command and exits 0. An # in-process updater is structurally a downloader -- fetch archive, extract, @@ -1178,7 +1178,7 @@ if [[ "$BINARY" == *.exe ]]; then else UPDATE_SCRIPT="install.sh" fi -if ! UPDATE_OUT=$(run_dryrun_env "$BINARY" update --dry-run --standard -y 2>&1); then +if ! UPDATE_OUT=$(run_dryrun_env "$BINARY" update --dry-run -y 2>&1); then echo "FAIL: update handoff exited non-zero" echo "$UPDATE_OUT" exit 1 @@ -3002,7 +3002,7 @@ echo "OK 9b-8: double uninstall doesn't crash" retire_account_daemon "9b-8-cleanup" smoke_rmtree "$DBL_HOME" -# 9b-9: Non-interactive update without --standard/--ui should fail cleanly (not hang) +# 9b-9: Non-interactive update must not hang (no variant prompt exists since #1538) if [ "$(uname -s)" != "MINGW64_NT" ] 2>/dev/null; then NONINT_OUT=$(echo "" | "$BINARY" update --dry-run 2>&1) || true if echo "$NONINT_OUT" | grep -qi 'terminal\|requires.*flag\|error'; then @@ -3226,11 +3226,7 @@ if [ -n "${SMOKE_DOWNLOAD_URL:-}" ]; then 'import json, os; print(json.dumps({"mcpServers":{"codebase-memory-mcp":{"command":os.environ["STALE_CMD"]}}}))' \ > "$UPDATE_HOME/.claude.json" - # 14a: Run actual update command (detect variant from available archive) - UPDATE_VARIANT="--standard" - if curl --noproxy '*' -sf "$SMOKE_DOWNLOAD_URL/" 2>/dev/null | grep -q "ui-"; then - UPDATE_VARIANT="--ui" - fi + # 14a: Run actual update command (one composition ships — no variant flag) UPDATE_LOG=$(smoke_mktemp_file) # Hash the driver BEFORE the run and compare it against itself afterwards. # Comparing against "$BINARY" instead looks equivalent but is not: the POSIX @@ -3238,7 +3234,7 @@ if [ -n "${SMOKE_DOWNLOAD_URL:-}" ]; then # is ever invoked and the assertion fires on a difference the fixture created. UPDATE_BIN_SHA_BEFORE=$(smoke_file_sha256 "$UPDATE_DRIVER") HOME="$UPDATE_HOME" CBM_DOWNLOAD_URL="$UPDATE_DOWNLOAD_URL" \ - "$UPDATE_DRIVER" update $UPDATE_VARIANT -y > "$UPDATE_LOG" 2>&1 + "$UPDATE_DRIVER" update -y > "$UPDATE_LOG" 2>&1 UPDATE_RC=$? cat "$UPDATE_LOG" @@ -3657,13 +3653,13 @@ UI_PID=$! UI_READY=0 for _ in $(seq 1 150); do if ! kill -0 "$UI_PID" 2>/dev/null; then break; fi - if curl -sf "http://127.0.0.1:$UI_PORT/" -o /dev/null 2>/dev/null; then UI_READY=1; break; fi + if curl --noproxy '*' -sf "http://127.0.0.1:$UI_PORT/" -o /dev/null 2>/dev/null; then UI_READY=1; break; fi sleep 0.2 done if [ "$UI_READY" -eq 1 ] || kill -0 "$UI_PID" 2>/dev/null; then # 15a: GET / returns 200 with HTML content - UI_BODY=$(curl -sf "http://127.0.0.1:$UI_PORT/" 2>/dev/null || echo "") + UI_BODY=$(curl --noproxy '*' -sf "http://127.0.0.1:$UI_PORT/" 2>/dev/null || echo "") if echo "$UI_BODY" | grep -qi "/dev/null; then # old probe POSTed an MCP initialize at /rpc, but the UI's /rpc speaks the # UI's own narrow query protocol, not MCP — the probe asserted a request # the endpoint never answered; protocol depth belongs to the UI guards.) - RPC_BODY=$(curl -sf "http://127.0.0.1:$UI_PORT/api/ui-config" 2>/dev/null || echo "") + RPC_BODY=$(curl --noproxy '*' -sf "http://127.0.0.1:$UI_PORT/api/ui-config" 2>/dev/null || echo "") if echo "$RPC_BODY" | grep -q "{"; then echo "OK 15b: /api/ui-config returns JSON" elif [ -z "$RPC_BODY" ]; then diff --git a/src/cli/activation_transaction.c b/src/cli/activation_transaction.c index 9ea1cfc6..d9af2185 100644 --- a/src/cli/activation_transaction.c +++ b/src/cli/activation_transaction.c @@ -1,5 +1,6 @@ /* Transactional binary activation. See activation_transaction.h. */ #include "cli/activation_transaction.h" +#include "foundation/log.h" #include "foundation/macos_acl.h" #include @@ -106,6 +107,22 @@ static void activation_note_refusal(const char *predicate, unsigned long os_erro g_activation_refusal_object ? g_activation_refusal_object : ""); } +#ifndef _WIN32 +/* A permission refusal has no errno to report — the syscall succeeded and the + * POLICY said no. Reporters spent hours chasing "I/O failed" for what was a + * mode bit (#1535), so these refusals carry the mode and the path instead of a + * fabricated OS error code. POSIX-only: the Windows validators refuse on ACL + * predicates and report through activation_note_refusal with a real OS error. */ +static void activation_note_refusal_detail(const char *predicate, const char *detail) { + if (g_activation_refusal_note[0] != '\0') { + return; + } + (void)snprintf(g_activation_refusal_note, sizeof(g_activation_refusal_note), "%s (%s)%s%s", + predicate, detail, g_activation_refusal_object ? " at " : "", + g_activation_refusal_object ? g_activation_refusal_object : ""); +} +#endif + const char *cbm_activation_transaction_refusal_note(void) { return g_activation_refusal_note; } @@ -740,11 +757,25 @@ static char *activation_posix_walk_path(const char *directory) { return activation_string_copy(directory); } +/* ANCESTOR policy (#1535). World-writable is still fatal: any local user could + * swap a path component mid-transaction. GROUP-writable is not — it is the + * default shape of ordinary home trees (WSL2 ships ~ and ~/.local at 0775, as + * do several distro skeletons and any site using a shared primary group), and + * refusing it made `install.sh` fail for a large fraction of Linux users with + * no actionable message. The group is a bounded, administratively-chosen set; + * the LEAF directory (below) stays strictly owner-private either way, so the + * binary itself is never left in a group-writable directory. Group-writable + * ancestors are warned about, out loud, rather than silently accepted. */ static bool activation_posix_intermediate_secure(const struct stat *status) { bool trusted_owner = status->st_uid == 0 || status->st_uid == geteuid(); - bool private_permissions = (status->st_mode & 0022) == 0; + bool world_writable = (status->st_mode & 0002) != 0; bool root_sticky = status->st_uid == 0 && (status->st_mode & S_ISVTX) != 0; - return S_ISDIR(status->st_mode) && trusted_owner && (private_permissions || root_sticky); + return S_ISDIR(status->st_mode) && trusted_owner && (!world_writable || root_sticky); +} + +static bool activation_posix_intermediate_group_writable(const struct stat *status) { + bool root_sticky = status->st_uid == 0 && (status->st_mode & S_ISVTX) != 0; + return (status->st_mode & 0020) != 0 && !root_sticky; } static bool activation_directory_secure(const char *directory, int *directory_fd_out, @@ -788,8 +819,23 @@ static bool activation_directory_secure(const char *directory, int *directory_fd while (*remaining == '/') { remaining++; } - if (next_ok && *remaining && !activation_posix_intermediate_secure(&next_status)) { - next_ok = false; + if (next_ok && *remaining) { + if (!activation_posix_intermediate_secure(&next_status)) { + char detail[64]; + (void)snprintf(detail, sizeof(detail), "mode %04o, uid %lu", + (unsigned)(next_status.st_mode & 07777), + (unsigned long)next_status.st_uid); + g_activation_refusal_object = walk_path; + activation_note_refusal_detail("ancestor_directory_world_writable", detail); + g_activation_refusal_object = NULL; + next_ok = false; + } else if (activation_posix_intermediate_group_writable(&next_status)) { + char mode_text[16]; + (void)snprintf(mode_text, sizeof(mode_text), "%04o", + (unsigned)(next_status.st_mode & 07777)); + cbm_log_warn("activation.ancestor_group_writable", "path", walk_path, "mode", + mode_text); + } } if (next_ok) { (void)close(descriptor); @@ -807,9 +853,36 @@ static bool activation_directory_secure(const char *directory, int *directory_fd } } struct stat status; - ok = ok && fstat(descriptor, &status) == 0 && S_ISDIR(status.st_mode) && - status.st_uid == geteuid() && (status.st_mode & 0022) == 0 && - activation_posix_acl_empty(descriptor); + if (ok && fstat(descriptor, &status) == 0) { + /* LEAF policy: strictly owner-private. This is the directory the binary + * is published into, so group/other write here would let another + * account replace the executable between validation and exec. Unlike + * the ancestors above, this one is refused — but it now says exactly + * which directory and which mode (#1535), instead of surfacing as a + * generic I/O failure that sent reporters hunting phantom disk errors. */ + bool is_dir = S_ISDIR(status.st_mode); + bool owned = status.st_uid == geteuid(); + bool private_permissions = (status.st_mode & 0022) == 0; + if (!is_dir || !owned || !private_permissions) { + char detail[64]; + (void)snprintf(detail, sizeof(detail), "mode %04o, uid %lu", + (unsigned)(status.st_mode & 07777), (unsigned long)status.st_uid); + g_activation_refusal_object = directory; + activation_note_refusal_detail(!is_dir ? "install_dir_not_a_directory" + : !owned ? "install_dir_not_owned_by_you" + : "install_dir_group_or_world_writable", + detail); + g_activation_refusal_object = NULL; + ok = false; + } else if (!activation_posix_acl_empty(descriptor)) { + g_activation_refusal_object = directory; + activation_note_refusal_detail("install_dir_carries_extra_acl_entries", "posix acl"); + g_activation_refusal_object = NULL; + ok = false; + } + } else { + ok = false; + } free(walk_path); if (!ok) { if (descriptor >= 0) { diff --git a/src/cli/cli.c b/src/cli/cli.c index 70ed44a1..010719a6 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -72,8 +72,6 @@ enum { MIN_ARGC_GET = 2, AUTO_YES = 1, AUTO_NO = -1, - VARIANT_A = 1, - VARIANT_B = 2, OCTAL_BASE = 8, CLI_ACTIVATION_DRAIN_TIMEOUT_MS = 15000, CLI_ACTIVATION_CONTROL_TIMEOUT_MS = 2000, @@ -154,9 +152,17 @@ int cbm_cli_exit_status_after_maintenance(int exit_status, bool maintenance_canc return maintenance_cancelled && exit_status == EXIT_SUCCESS ? EXIT_FAILURE : exit_status; } +/* #1537: the refusal is correct, but "sessions could not be stopped" gives the + * reader nothing to act on — which sessions? The daemon already knows every + * client pid and `daemon status` prints them, so name that remedy here rather + * than leaving people to guess (one reporter uninstalled first and still hit + * this, with no way to see what was holding on). */ static const char CLI_ACTIVATION_REFUSED_MESSAGE[] = "error: active CBM sessions and operations could not be stopped safely; " - "no activation was committed."; + "no activation was committed.\n" + "error: run 'codebase-memory-mcp daemon status' to list the client " + "processes still holding the daemon (MCP servers started by your editor or " + "agent are the usual holders), close them, then retry."; static const char CLI_ACTIVATION_PARTIAL_MESSAGE[] = "error: activation stopped after one or more agent configuration or " "cleanup operations failed; the published/current executable was kept, " @@ -11294,7 +11300,7 @@ static int extract_and_install_binary(extract_install_args_t args) { * covers the flow through the activation test seam. */ /* Build the download URL for the update command. */ static void build_update_url(char *url, int url_sz, const char *os, const char *arch, - const char *ext, bool want_ui) { + const char *ext) { char base_url_buf[CLI_BUF_512]; const char *base_url = cbm_safe_getenv("CBM_DOWNLOAD_URL", base_url_buf, sizeof(base_url_buf), NULL); @@ -11306,8 +11312,7 @@ static void build_update_url(char *url, int url_sz, const char *os, const char * * have no such variant. Keep in sync with install.sh / install.js / pypi * _cli.py. */ const char *portable = (strcmp(os, "linux") == 0) ? "-portable" : ""; - snprintf(url, url_sz, "%s/codebase-memory-mcp-%s%s-%s%s.%s", base_url, want_ui ? "ui-" : "", os, - arch, portable, ext); + snprintf(url, url_sz, "%s/codebase-memory-mcp-%s-%s%s.%s", base_url, os, arch, portable, ext); } /* Confirm index deletion before network I/O, but defer the deletion itself to @@ -11340,8 +11345,8 @@ static int update_prepare_clear_indexes(const char *home, bool dry_run, bool *de /* Download and verify before disruption, then activate under daemon locks. */ static int download_verify_install(const char *url, const char *ext, const char *os, - const char *arch, bool want_ui, const char *bin_dest, - const char *home, bool delete_indexes) { + const char *arch, const char *bin_dest, const char *home, + bool delete_indexes) { char tmp_archive[CLI_BUF_256]; int archive_path_length = snprintf(tmp_archive, sizeof(tmp_archive), "%s/cbm-update-XXXXXX", cbm_tmpdir()); @@ -11372,8 +11377,8 @@ static int download_verify_install(const char *url, const char *ext, const char char archive_name[CLI_BUF_256]; /* Must match build_update_url: linux uses the static "-portable" asset. */ const char *portable = (strcmp(os, "linux") == 0) ? "-portable" : ""; - snprintf(archive_name, sizeof(archive_name), "codebase-memory-mcp-%s%s-%s%s.%s", - want_ui ? "ui-" : "", os, arch, portable, ext); + snprintf(archive_name, sizeof(archive_name), "codebase-memory-mcp-%s-%s%s.%s", os, arch, + portable, ext); /* Fail closed: install only a positively-verified download. A mismatch, * a missing checksum entry, or an unavailable hash tool (crc != 0) all * abort rather than install an unverified binary. */ @@ -11396,34 +11401,6 @@ static int download_verify_install(const char *url, const char *ext, const char return 0; } -/* Select update variant. Returns 0=standard, 1=ui, -1=error. */ -static int select_update_variant(int variant_flag) { - if (variant_flag == VARIANT_A) { - return 0; - } - if (variant_flag == VARIANT_B) { - return CLI_TRUE; - } -#ifndef _WIN32 - if (!isatty(fileno(stdin))) { - (void)fprintf(stderr, "error: variant selection requires a terminal. " - "Use --standard or --ui flag.\n"); - return CLI_ERR; - } -#endif - printf("Which binary variant do you want?\n"); - printf(" 1) standard — MCP server only\n"); - printf(" 2) ui — MCP server + embedded graph visualization\n"); - printf("Choose (1/2): "); - (void)fflush(stdout); - char choice[CLI_BUF_16]; - if (!fgets(choice, sizeof(choice), stdin)) { - (void)fprintf(stderr, "error: failed to read input\n"); - return CLI_ERR; - } - return (choice[0] == '2') ? CLI_TRUE : 0; -} - /* Case-insensitive prefix match (portable — no strncasecmp dependency). */ static bool prefix_icase(const char *s, const char *prefix) { while (*prefix) { @@ -11505,14 +11482,9 @@ int cbm_cmd_update(int argc, char **argv) { bool dry_run = false; bool force = false; - int variant_flag = 0; /* 0 = ask, 1 = standard, 2 = ui */ for (int i = 0; i < argc; i++) { if (strcmp(argv[i], "--dry-run") == 0) { dry_run = true; - } else if (strcmp(argv[i], "--standard") == 0) { - variant_flag = VARIANT_A; - } else if (strcmp(argv[i], "--ui") == 0) { - variant_flag = VARIANT_B; } else if (strcmp(argv[i], "--force") == 0) { force = true; } else if (strcmp(argv[i], "-y") != 0 && strcmp(argv[i], "--yes") != 0 && @@ -11546,7 +11518,6 @@ int cbm_cmd_update(int argc, char **argv) { * rejecting typos instead of silently accepting them. */ (void)dry_run; (void)force; - (void)variant_flag; #endif #ifdef CBM_CLI_ENABLE_TEST_API if (g_cli_activation_test_ops_set) { @@ -11602,7 +11573,8 @@ int cbm_cmd_update(int argc, char **argv) { } printf("It downloads the latest release, verifies its checksum, and replaces\n" "this binary in place. install.sh is idempotent, so re-running it IS\n" - "the update; pass --ui for the UI build.\n"); + "the update. One build per platform — the graph UI is always\n" + "included.\n"); #endif return 0; } @@ -11631,26 +11603,17 @@ int cbm_cmd_update(int argc, char **argv) { return CLI_TRUE; } - /* Step 2: Determine variant */ - int want_ui_rc = select_update_variant(variant_flag); - if (want_ui_rc < 0) { - return CLI_TRUE; - } - bool want_ui = (want_ui_rc == CLI_TRUE); - const char *variant = want_ui ? "ui-" : ""; - const char *variant_label = want_ui ? "ui" : "standard"; - const char *os = detect_os(); const char *arch = detect_arch(); const char *ext = strcmp(os, "windows") == 0 ? "zip" : "tar.gz"; char url[CLI_BUF_512]; - build_update_url(url, sizeof(url), os, arch, ext, want_ui); + build_update_url(url, sizeof(url), os, arch, ext); if (dry_run) { - printf("\nWould download %s binary for %s/%s ...\n", variant_label, os, arch); + printf("\nWould download the binary for %s/%s ...\n", os, arch); } else { - printf("\nDownloading %s binary for %s/%s ...\n", variant_label, os, arch); + printf("\nDownloading the binary for %s/%s ...\n", os, arch); } printf(" %s\n", url); @@ -11661,10 +11624,8 @@ int cbm_cmd_update(int argc, char **argv) { #else printf(" target: %s/.local/bin/codebase-memory-mcp\n", home); #endif - printf(" variant: %s\n", variant_label); printf(" os/arch: %s/%s\n", os, arch); printf("\nUpdate dry-run complete.\n"); - (void)variant; return 0; } @@ -11684,7 +11645,7 @@ int cbm_cmd_update(int argc, char **argv) { return CLI_TRUE; } - int rc = download_verify_install(url, ext, os, arch, want_ui, bin_dest, home, delete_indexes); + int rc = download_verify_install(url, ext, os, arch, bin_dest, home, delete_indexes); if (rc != 0) { return CLI_TRUE; } @@ -11701,7 +11662,6 @@ int cbm_cmd_update(int argc, char **argv) { printf("automatically when you next use the MCP server.\n"); printf("\nUpdate complete. Please restart your coding-agent sessions to " "properly take this into account.\n"); - (void)variant; return 0; #endif /* CBM_CLI_ENABLE_TEST_API */ } diff --git a/src/daemon/runtime.c b/src/daemon/runtime.c index bdbb2c56..e62b1c7d 100644 --- a/src/daemon/runtime.c +++ b/src/daemon/runtime.c @@ -28,6 +28,14 @@ static atomic_bool runtime_force_peer_image_unverified_seam; void cbm_daemon_runtime_force_peer_image_unverified_for_testing(bool force) { atomic_store(&runtime_force_peer_image_unverified_seam, force); } +/* The two failure modes are NOT interchangeable and must be testable apart: + * an image that cannot be examined at all is admitted (the peer already proved + * build compatibility in the HELLO), while one that CAN be examined and differs + * is rejected. One seam per mode keeps each contract honest. */ +static atomic_bool runtime_force_peer_image_mismatch_seam; +void cbm_daemon_runtime_force_peer_image_mismatch_for_testing(bool force) { + atomic_store(&runtime_force_peer_image_mismatch_seam, force); +} #endif #ifdef _WIN32 @@ -1752,11 +1760,32 @@ static void *runtime_connection_worker(void *opaque) { peer_image_verified = false; peer_image_fingerprinted = false; } + if (atomic_load(&runtime_force_peer_image_mismatch_seam)) { + peer_image_verified = false; + peer_image_fingerprinted = true; + } #endif + /* Two different failures wear the same "unverified" flag, and treating them + * alike broke every ephemeral-path client (#1539/#1383): + * + * fingerprint_mismatch — the peer's image WAS read and hashes differently + * than the running daemon. That is the tamper/skew case the gate exists + * for. Still rejected, hard. + * image_unverifiable — the peer's image could not be examined at all + * (ephemeral npx cache paths, ptrace_scope restrictions, sandboxed + * hosts). Nothing was contradicted; we simply could not look. The peer + * ALREADY proved semantic version, build fingerprint, protocol/store/ + * feature ABI and cache root in the HELLO exchange above — rejecting on + * top of that traded a real compatibility proof for an unavailable one, + * and made `npx codebase-memory-mcp` unusable with the daemon. Admit, + * and say so out loud so the weaker check is never invisible. */ + if (!peer_image_verified && !peer_image_fingerprinted) { + cbm_log_warn("daemon.client_image_unverifiable_admitted", "reason", "image_unverifiable", + "basis", "rendezvous_hello_verified"); + peer_image_verified = true; + } if (!peer_image_verified) { - const char *reason = - peer_image_fingerprinted ? "fingerprint_mismatch" : "image_unverifiable"; - cbm_log_error("daemon.client_image_rejected", "reason", reason); + cbm_log_error("daemon.client_image_rejected", "reason", "fingerprint_mismatch"); /* #1383: answer the peer before closing. An unanswered rejection is * indistinguishable from a slow cold start on the client side — the * caller sat on "pending" indefinitely with the reason visible only in @@ -1765,10 +1794,9 @@ static void *runtime_connection_worker(void *opaque) { * peer; admission stays rejected either way. */ runtime_result_rejected(&hello_result, "CBM daemon rejected this client's binary image"); (void)snprintf(hello_result.message, sizeof(hello_result.message), - "CBM daemon rejected this client: %s. The client binary must match the " - "running daemon's build; close CBM sessions (or run 'daemon stop') and " - "retry with one consistent install.", - reason); + "CBM daemon rejected this client: fingerprint_mismatch. The client binary " + "must match the running daemon's build; close CBM sessions (or run " + "'daemon stop') and retry with one consistent install."); (void)runtime_send_hello_response(worker->connection, &hello_result); runtime_worker_finish(worker); return NULL; diff --git a/src/daemon/runtime.h b/src/daemon/runtime.h index 31eb8d79..2fd81889 100644 --- a/src/daemon/runtime.h +++ b/src/daemon/runtime.h @@ -415,6 +415,10 @@ bool cbm_daemon_runtime_client_close(cbm_daemon_runtime_client_t *client, uint32 /* #1383 test seam: force peer-image verification to fail so tests can exercise * the rejection-response path from the in-process harness. */ void cbm_daemon_runtime_force_peer_image_unverified_for_testing(bool force); +/* #1539 test seam: force the peer image to be FINGERPRINTABLE but different, + * i.e. the tamper case that must still be rejected after unverifiable images + * became admissible. */ +void cbm_daemon_runtime_force_peer_image_mismatch_for_testing(bool force); #endif #endif /* CBM_DAEMON_RUNTIME_H */ diff --git a/src/main.c b/src/main.c index aff4adbb..574002b6 100644 --- a/src/main.c +++ b/src/main.c @@ -1442,6 +1442,45 @@ static bool main_semver_newer(const char *candidate, const char *active) { return false; } +/* A client that cannot reach the daemon must SAY SO, in the caller's own + * protocol. An MCP client speaks JSON-RPC over stdout, and the old path + * returned EXIT_FAILURE having written nothing at all: agents saw a transport + * that closed mid-handshake and reported "Connection closed" with no cause, + * while the real reason (image rejection, startup timeout, conflict) sat in + * bootstrap_result.message and was dropped on the floor (#1539). + * + * stdout carries a JSON-RPC error object so the agent surfaces the reason; + * id is null because the failure precedes reading any request. stderr carries + * the same text for humans reading a terminal. */ +static void main_report_client_bootstrap_failure(cbm_daemon_process_role_t role, + const cbm_daemon_bootstrap_result_t *result) { + const char *detail = (result && result->message[0]) + ? result->message + : "CBM daemon connection failed before the session was established"; + if (cbm_daemon_process_role_requires_client(role)) { + char escaped[CBM_DAEMON_CONFLICT_MESSAGE_SIZE * 2]; + size_t out = 0; + for (size_t i = 0; detail[i] && out + 2 < sizeof(escaped); i++) { + unsigned char c = (unsigned char)detail[i]; + if (c == '"' || c == '\\') { + escaped[out++] = '\\'; + escaped[out++] = (char)c; + } else if (c < 0x20) { + escaped[out++] = ' '; + } else { + escaped[out++] = (char)c; + } + } + escaped[out] = '\0'; + (void)fprintf(stdout, + "{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32001," + "\"message\":\"%s\"}}\n", + escaped); + (void)fflush(stdout); + } + (void)fprintf(stderr, "codebase-memory-mcp: %s\n", detail); +} + /* Client bootstrap with the upgrade policy: a CONFLICT against a PERMANENT * daemon of a strictly OLDER release is resolved by draining that daemon * (the same authenticated path install/update use) and retrying once. A @@ -2798,6 +2837,7 @@ int main(int argc, char **argv) { main_client_bootstrap_with_upgrade(&bootstrap_config, &bootstrap_result); cbm_daemon_ipc_endpoint_free(endpoint); if (bootstrap_status != CBM_DAEMON_BOOTSTRAP_CONNECTED || !bootstrap_result.client) { + main_report_client_bootstrap_failure(role, &bootstrap_result); (void)main_version_cohort_close(&client_cohort_lease, &client_cohort_manager); return EXIT_FAILURE; } diff --git a/tests/test_activation_transaction.c b/tests/test_activation_transaction.c index c73ae3b8..b984145f 100644 --- a/tests/test_activation_transaction.c +++ b/tests/test_activation_transaction.c @@ -608,6 +608,68 @@ TEST(activation_transaction_rejects_cross_account_writable_target_directory) { PASS(); } +/* #1535: the refusal above is correct, but for a year it surfaced to users as + * "activation transaction I/O failed" — a policy decision wearing an I/O + * costume. Reporters chased disk errors and filesystem types for a mode bit. + * The refusal must name the directory, its mode, and which rule refused. */ +TEST(activation_transaction_permission_refusal_names_directory_and_mode) { +#ifndef _WIN32 + char directory[ACTIVATION_TEST_PATH_CAP]; + char target[ACTIVATION_TEST_PATH_CAP]; + ASSERT_TRUE(activation_test_fixture(directory)); + ASSERT_TRUE(activation_test_path(target, directory, "cbm")); + /* Group-writable LEAF: still refused (the binary lands here), unlike a + * group-writable ancestor which is now only warned about. */ + ASSERT_EQ(chmod(directory, 0775), 0); + cbm_activation_transaction_t *transaction = NULL; + ASSERT_EQ(cbm_activation_transaction_stage_bytes(target, "candidate", strlen("candidate"), + &transaction), + CBM_ACTIVATION_TRANSACTION_IO); + ASSERT_NULL(transaction); + + const char *note = cbm_activation_transaction_refusal_note(); + ASSERT_NOT_NULL(note); + ASSERT_TRUE(note[0] != '\0'); + /* WHICH rule refused, WHAT the mode was, and WHERE — all three, or the + * message is back to sending people after phantom disk failures. */ + ASSERT_NOT_NULL(strstr(note, "install_dir_group_or_world_writable")); + ASSERT_NOT_NULL(strstr(note, "0775")); + ASSERT_NOT_NULL(strstr(note, directory)); + + ASSERT_EQ(chmod(directory, 0700), 0); + ASSERT_EQ(th_rmtree(directory), 0); +#endif + PASS(); +} + +/* #1535: a group-writable ANCESTOR is the default shape of WSL2 and several + * distro home trees. Refusing it broke install.sh for a large fraction of Linux + * users; it is now warned about and admitted, while the leaf stays private. */ +TEST(activation_transaction_admits_group_writable_ancestor) { +#ifndef _WIN32 + char parent[ACTIVATION_TEST_PATH_CAP]; + char child[ACTIVATION_TEST_PATH_CAP]; + char target[ACTIVATION_TEST_PATH_CAP]; + ASSERT_TRUE(activation_test_fixture(parent)); + ASSERT_TRUE(activation_test_path(child, parent, "bin")); + ASSERT_TRUE(cbm_mkdir_p(child, 0700)); + ASSERT_TRUE(activation_test_path(target, child, "cbm")); + ASSERT_EQ(chmod(parent, 0775), 0); + + cbm_activation_transaction_t *transaction = NULL; + ASSERT_EQ(cbm_activation_transaction_stage_bytes(target, "candidate", strlen("candidate"), + &transaction), + CBM_ACTIVATION_TRANSACTION_OK); + ASSERT_NOT_NULL(transaction); + (void)cbm_activation_transaction_rollback(transaction); + ASSERT_EQ(cbm_activation_transaction_close(&transaction), CBM_ACTIVATION_TRANSACTION_OK); + + ASSERT_EQ(chmod(parent, 0700), 0); + ASSERT_EQ(th_rmtree(parent), 0); +#endif + PASS(); +} + TEST(activation_transaction_rejects_windows_callback_allow_directory_ace) { #ifdef _WIN32 char directory[ACTIVATION_TEST_PATH_CAP]; @@ -1023,6 +1085,8 @@ SUITE(activation_transaction) { RUN_TEST(activation_transaction_removal_survives_transient_rename_locks); #endif RUN_TEST(activation_transaction_rejects_cross_account_writable_target_directory); + RUN_TEST(activation_transaction_permission_refusal_names_directory_and_mode); + RUN_TEST(activation_transaction_admits_group_writable_ancestor); RUN_TEST(activation_transaction_rejects_windows_callback_allow_directory_ace); RUN_TEST(activation_transaction_rejects_symlink_candidate_target_and_parent); RUN_TEST(activation_transaction_fails_closed_if_target_directory_is_replaced); diff --git a/tests/test_cli.c b/tests/test_cli.c index 076ee219..9baf9eae 100644 --- a/tests/test_cli.c +++ b/tests/test_cli.c @@ -1383,8 +1383,8 @@ TEST(cli_update_download_failure_does_not_quiesce_sessions) { }; cbm_cli_activation_ops_t ops = cli_activation_fake_ops(&fake); cbm_cli_set_activation_ops_for_test(&ops); - char *argv[] = {"--force", "--standard", "--yes"}; - int rc = cli_test_cmd_update(3, argv); + char *argv[] = {"--force", "--yes"}; + int rc = cli_test_cmd_update(2, argv); cbm_cli_set_activation_ops_for_test(NULL); cbm_set_auto_answer_for_test(0); @@ -1444,7 +1444,7 @@ TEST(cli_update_already_current_does_not_quiesce_sessions) { }; cbm_cli_activation_ops_t ops = cli_activation_fake_ops(&fake); cbm_cli_set_activation_ops_for_test(&ops); - char *argv[] = {"--standard"}; + char *argv[] = {"--yes"}; int rc = fixture_ready ? cli_test_cmd_update(1, argv) : -1; cbm_cli_set_activation_ops_for_test(NULL); @@ -1576,8 +1576,8 @@ TEST(cli_update_agent_configs_finish_before_guard_release) { }; cbm_cli_activation_ops_t ops = cli_activation_fake_ops(&fake); cbm_cli_set_activation_ops_for_test(&ops); - char *argv[] = {"--force", "--standard"}; - int rc = cli_test_cmd_update(2, argv); + char *argv[] = {"--force"}; + int rc = cli_test_cmd_update(1, argv); cbm_cli_set_activation_ops_for_test(NULL); /* Re-run against a known old target while one independently detected agent @@ -1599,7 +1599,7 @@ TEST(cli_update_agent_configs_finish_before_guard_release) { }; cbm_cli_activation_ops_t failure_ops = cli_activation_fake_ops(&config_failure); cbm_cli_set_activation_ops_for_test(&failure_ops); - int config_failure_rc = cli_test_cmd_update(2, argv); + int config_failure_rc = cli_test_cmd_update(1, argv); cbm_cli_set_activation_ops_for_test(NULL); struct stat updated_status; bool replacement_kept = stat(bin_target, &updated_status) == 0 && @@ -1767,11 +1767,11 @@ TEST(cli_activation_guard_is_bypassed_for_dry_run_and_plan) { cbm_cli_set_activation_ops_for_test(&ops); char *install_dry[] = {"--force", "--dry-run"}; char *install_plan[] = {"--force", "--plan"}; - char *update_dry[] = {"--force", "--dry-run", "--standard"}; + char *update_dry[] = {"--force", "--dry-run"}; char *uninstall_dry[] = {"--dry-run", "--yes"}; int install_dry_rc = cli_test_cmd_install(2, install_dry); int install_plan_rc = cli_test_cmd_install(2, install_plan); - int update_dry_rc = cli_test_cmd_update(3, update_dry); + int update_dry_rc = cli_test_cmd_update(2, update_dry); int uninstall_dry_rc = cli_test_cmd_uninstall(2, uninstall_dry); cbm_cli_set_activation_ops_for_test(NULL); cbm_set_auto_answer_for_test(0); diff --git a/tests/test_daemon_runtime.c b/tests/test_daemon_runtime.c index c97c5ad1..5f41d914 100644 --- a/tests/test_daemon_runtime.c +++ b/tests/test_daemon_runtime.c @@ -1850,7 +1850,10 @@ TEST(daemon_runtime_exact_hello_issues_connection_bound_identity) { * silently dropped. The old path logged daemon.client_image_rejected and * finished the worker without sending a hello response, so the client sat on * "pending" indefinitely - indistinguishable from a slow cold start - with the - * reason visible only in the daemon log. */ + * reason visible only in the daemon log. + * + * The rejection is now scoped to fingerprint_mismatch (see #1539 below), so + * this drives the seam that keeps a peer image readable but DIFFERENT. */ TEST(daemon_runtime_image_rejection_reaches_client_issue1383) { cbm_daemon_build_identity_t identity = runtime_test_identity("2.4.0", runtime_test_self_build()); @@ -1859,6 +1862,40 @@ TEST(daemon_runtime_image_rejection_reaches_client_issue1383) { cbm_daemon_runtime_connect_result_t result = {0}; cbm_daemon_runtime_client_t *client = NULL; + cbm_daemon_runtime_force_peer_image_mismatch_for_testing(true); + if (started) { + client = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity, + RUNTIME_TEST_TIMEOUT_MS, &result); + } + cbm_daemon_runtime_force_peer_image_mismatch_for_testing(false); + + bool rejected_with_reason = client == NULL && + result.status == CBM_DAEMON_RUNTIME_CONNECT_REJECTED && + strstr(result.message, "fingerprint_mismatch") != NULL; + if (client) { + (void)cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS); + } + runtime_test_fixture_finish(&fixture); + + ASSERT_TRUE(started); + ASSERT_TRUE(rejected_with_reason); + PASS(); +} + +/* #1539: a peer whose image cannot be EXAMINED is not a peer that failed a + * check — it is a peer we could not look at. Every `npx codebase-memory-mcp` + * invocation lands here (ephemeral cache path, unfingerprintable), and the old + * gate rejected all of them: the MCP client saw a 30 s wait and zero bytes. + * The HELLO exchange that already succeeded proves version, build fingerprint + * and ABI compatibility, so admission is the honest outcome. */ +TEST(daemon_runtime_unverifiable_image_is_admitted_issue1539) { + cbm_daemon_build_identity_t identity = + runtime_test_identity("2.4.0", runtime_test_self_build()); + runtime_test_fixture_t fixture; + bool started = runtime_test_fixture_start(&fixture, "image-unverifiable", &identity); + cbm_daemon_runtime_connect_result_t result = {0}; + cbm_daemon_runtime_client_t *client = NULL; + cbm_daemon_runtime_force_peer_image_unverified_for_testing(true); if (started) { client = cbm_daemon_runtime_client_connect(fixture.endpoint, &identity, @@ -1866,16 +1903,14 @@ TEST(daemon_runtime_image_rejection_reaches_client_issue1383) { } cbm_daemon_runtime_force_peer_image_unverified_for_testing(false); - bool rejected_with_reason = client == NULL && - result.status == CBM_DAEMON_RUNTIME_CONNECT_REJECTED && - strstr(result.message, "image_unverifiable") != NULL; + bool admitted = client != NULL && result.status == CBM_DAEMON_RUNTIME_CONNECT_ACCEPTED; if (client) { - (void)cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS); + admitted = cbm_daemon_runtime_client_close(client, RUNTIME_TEST_TIMEOUT_MS) && admitted; } runtime_test_fixture_finish(&fixture); ASSERT_TRUE(started); - ASSERT_TRUE(rejected_with_reason); + ASSERT_TRUE(admitted); PASS(); } @@ -4720,6 +4755,7 @@ SUITE(daemon_runtime) { RUN_TEST(daemon_runtime_rendezvous_layout_is_frozen_and_detailed_abi_independent); RUN_TEST(daemon_runtime_exact_hello_issues_connection_bound_identity); RUN_TEST(daemon_runtime_image_rejection_reaches_client_issue1383); + RUN_TEST(daemon_runtime_unverifiable_image_is_admitted_issue1539); RUN_TEST(daemon_runtime_unexpected_frame_payload_is_freed_once); RUN_TEST(daemon_runtime_activation_rejects_forged_and_malformed_without_stop); RUN_TEST(daemon_runtime_activation_ack_snapshots_then_interrupts_all_clients); diff --git a/tests/test_daemon_smoke.py b/tests/test_daemon_smoke.py index ebde8a77..93ab933a 100644 --- a/tests/test_daemon_smoke.py +++ b/tests/test_daemon_smoke.py @@ -2135,7 +2135,7 @@ def main(): activation_log, "activation-update", "update", - ["update", "--force", "--standard", "--yes"], + ["update", "--force", "--yes"], active_fingerprint, ) check( diff --git a/tests/test_smoke_fixture_contract.sh b/tests/test_smoke_fixture_contract.sh index b202f967..ce66848f 100755 --- a/tests/test_smoke_fixture_contract.sh +++ b/tests/test_smoke_fixture_contract.sh @@ -311,9 +311,25 @@ require( and "& $args[1]" not in smoke_test, "Windows Phase 13 must execute install.ps1 directly with native paths", ) +# A count cannot tell "we still have four curls" from "every curl is safe" — it +# passed while a loopback UI probe carried no --noproxy at all, which is the +# exact failure this rule exists to prevent (an ambient http_proxy makes a +# 127.0.0.1 request leave the machine). Assert the property on every invocation +# instead, so adding or removing a curl cannot silently satisfy the rule. +unproxied_curls = [ + line.strip() + for line in smoke_test.splitlines() + # An invocation starts a command: line start, or after ; & | ( or `if`/`then` + # etc. Mentions inside echo/comment strings are not invocations. + if re.search(r"(?:^|[;&|(]|\b(?:if|then|else|do|not)\s)\s*curl\s", line) + and not line.lstrip().startswith("#") + and not re.search(r"echo\s", line.split("curl")[0]) + and "--noproxy" not in line +] require( - smoke_test.count("--noproxy '*'") >= 4, - "all loopback release-fixture curl requests must bypass ambient proxies", + not unproxied_curls, + "every curl in the smoke fixture must bypass ambient proxies (--noproxy '*'): " + + "; ".join(unproxied_curls[:3]), ) require( "/tmp/cbm-curl12a.err" not in smoke_test diff --git a/tests/windows/test_windows_update_handoff.py b/tests/windows/test_windows_update_handoff.py index b0b36a6f..6a925c29 100644 --- a/tests/windows/test_windows_update_handoff.py +++ b/tests/windows/test_windows_update_handoff.py @@ -116,9 +116,9 @@ def assert_update_hands_off_to_install_script(source, env, work): # of the just-written image inflates process load time, and the handoff # itself is a fast STATELESS local print (no daemon IPC, no download) whose # timing is what this guard measures. The warm-up behaves identically. - run([binary, "update", "--yes", "--standard"], command_env, timeout=20) + run([binary, "update", "--yes"], command_env, timeout=20) started = time.monotonic() - result = run([binary, "update", "--yes", "--standard"], command_env, timeout=20) + result = run([binary, "update", "--yes"], command_env, timeout=20) elapsed = time.monotonic() - started diagnostic = output_text(result) lowered = diagnostic.lower() @@ -165,7 +165,7 @@ def assert_update_does_not_drain_active_session(source, env, cache, work): command = copy_binary(source, work / "update-session-command") command_env = dict(env) command_env["CBM_DOWNLOAD_URL"] = "https://127.0.0.1:1" - result = run([command, "update", "--yes", "--standard"], command_env, timeout=20) + result = run([command, "update", "--yes"], command_env, timeout=20) require( result.returncode == 0, "update exited %s beside a live session: %s"