fix(install,daemon): unbreak npx clients, group-writable homes, and legacy updaters

Five field reports in the 24 hours after v0.10.0 all pointed at the same thing:
gates that were right in principle refused real, ordinary setups, and then
failed to say why. Per the consolidated strictness decision, each gate keeps the
protection that matters and drops the part that was refusing legitimate users —
and every refusal now names what it refused and how to proceed.

**Daemon image gate: npx and every ephemeral install path (#1539, #1383).**
The admission check treated "the peer's image hashes differently" and "the
peer's image cannot be examined at all" as one failure. The second is what
`npx codebase-memory-mcp` always produces (ephemeral cache path,
unfingerprintable), so every npx-invoked client was rejected — and, because the
client never reported it, agents saw a transport that closed mid-handshake with
zero bytes on stdout. Reported by @wassolles with the admission path already
read and the fix space mapped.

An unverifiable image is now admitted: the rendezvous HELLO immediately above it
has already proven semantic version, build fingerprint, and protocol/store/
feature ABI, and the image check was trading that real proof for an unavailable
one. It logs daemon.client_image_unverifiable_admitted so the weaker check is
never invisible. A fingerprint MISMATCH — the tamper case the gate exists for —
still rejects hard. Separate test seams keep the two modes testable apart.

**Client bootstrap failures are no longer silent (#1539).**
An MCP client that cannot reach the daemon now emits a JSON-RPC error on stdout
naming the reason, plus the same text on stderr. Previously the reason sat in
bootstrap_result.message and the process exited having written nothing at all.

**POSIX activation: group-writable ancestors (#1535, discussion #1526).**
activation_directory_secure required no group or other write bit on the install
directory AND every ancestor. WSL2 ships ~ and ~/.local at 0775, as do several
distro skeletons and any site using a shared primary group, so install.sh failed
for a large fraction of Linux users — reporting a policy refusal as "activation
transaction I/O failed", which sent reporters after disk errors and filesystem
types. Root-caused by @AmirF194 in a clean ubuntu container; @shochdoerfer and
@iandol confirmed independently.

World-writable ancestors are still refused (any local user could swap a path
component mid-transaction). Group-writable ancestors are now warned about and
admitted. The LEAF directory stays strictly owner-private — that is where the
binary is published, and group write there would let another account replace the
executable between validation and exec. Refusals now name the directory, its
mode, and which rule refused.

**The obsolete ui/standard chooser (#1538, from discussion #1526).**
v0.10.0 consolidated to one archive per platform with the UI always embedded,
but `update` still offered a variant choice: "ui" could only 404, and "standard"
quietly WAS the UI build. Reported by @iandol upgrading 0.9.0 -> 0.10.0. The
chooser, its --standard/--ui flags, and the ui- URL plumbing are removed, along
with the CBM_VARIANT=ui remnant in the npm installer.

Already-released 0.9.x binaries cannot be fixed retroactively, so the release
workflow now publishes byte-identical ui-*-named alias assets — their updaters
work again with no user action. The aliases are uploaded AFTER the VirusTotal
gate: they are the same bytes as archives it already cleared, and uploading them
earlier would duplicate every object in the scan set and the provenance manifest.

**macOS install noise and attribution (#1537).**
install.sh silenced the "No such xattr: com.apple.quarantine" line, which is
what happens when a curl-downloaded archive carries no quarantine attribute —
harmless, and it became the title of a bug report about an unrelated failure.
The session-stop refusal now points at `daemon status` to list the client
processes actually holding the daemon, instead of asserting sessions exist and
leaving the reader to guess. Reported by @listepo.

**Riders.** hatchling is pinned in pkg/pypi (an unpinned backend resolved fresh
inside `python -m build` is what emitted Metadata-Version 2.5 and broke the
v0.10.1 publish); SECURITY.md's supported-versions table moves to 0.10.x.

Tests: separate seams for unverifiable vs mismatched peer images with a test per
outcome; activation refusal must name directory + mode + rule; a group-writable
ancestor must stage successfully. The update tests drop the flag that no longer
exists. Verified against each reporter's environment shape.

**Open security alerts (all three, OSSF Scorecard).**
- HIGH, binary artifact: an 8.8 MB compiled Go ELF wrapper had been committed at
  pkg/go/codebase-memory-mcp by accident. Removed, and both it and its .exe
  sibling are gitignored so `go build` in that directory cannot repeat it.
- HIGH, GHSA-2v37-7h3g-55p8: nanoid < 3.3.17 loops forever when a custom
  generator is called with size 0. It reaches us transitively (postcss -> vite),
  so it is pinned through the existing graph-ui overrides block rather than
  promoted to a direct dependency; the lockfile resolves 3.3.18.
- MEDIUM, unpinned pip command: the publish step installed build/twine by
  version only, leaving the whole transitive graph resolved at run time.
  pkg/pypi/requirements-publish.txt now hash-pins the complete toolchain (316
  hashes), generated on a linux/amd64 python:3.12 image so the wheels match what
  ubuntu-latest resolves, and the step runs pip with --require-hashes. Verified
  by installing from it in that same image.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-08-11 13:18:47 +02:00
parent 564d32cc87
commit 8eabe191d2
25 changed files with 784 additions and 138 deletions
+21 -9
View File
@@ -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 }}
+6
View File
@@ -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
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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": [
{
+2 -1
View File
@@ -41,6 +41,7 @@
},
"overrides": {
"form-data": ">=4.0.6",
"@babel/core": ">=7.29.6"
"@babel/core": ">=7.29.6",
"nanoid": "^3.3.17"
}
}
+7 -2
View File
@@ -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
Binary file not shown.
+1 -5
View File
@@ -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:
+5 -6
View File
@@ -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}`);
+2 -2
View File
@@ -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
+5 -1
View File
@@ -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]
+359
View File
@@ -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
+47
View File
@@ -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-<platform>.<ext> 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 <version-tag> <repository>
set -euo pipefail
VERSION="${1:?usage: publish-legacy-aliases.sh <version-tag> <repository>}"
REPOSITORY="${2:?usage: publish-legacy-aliases.sh <version-tag> <repository>}"
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"
+8 -12
View File
@@ -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 "<html"; then
echo "OK 15a: UI serves HTML at /"
elif [ -z "$UI_BODY" ]; then
@@ -3679,7 +3675,7 @@ if [ "$UI_READY" -eq 1 ] || kill -0 "$UI_PID" 2>/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
+80 -7
View File
@@ -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 <errno.h>
@@ -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) {
+21 -61
View File
@@ -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 */
}
+35 -7
View File
@@ -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;
+4
View File
@@ -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 */
+40
View File
@@ -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;
}
+64
View File
@@ -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);
+8 -8
View File
@@ -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);
+42 -6
View File
@@ -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);
+1 -1
View File
@@ -2135,7 +2135,7 @@ def main():
activation_log,
"activation-update",
"update",
["update", "--force", "--standard", "--yes"],
["update", "--force", "--yes"],
active_fingerprint,
)
check(
+18 -2
View File
@@ -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
+3 -3
View File
@@ -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"