Skip to content

fix(install,daemon): unbreak npx clients, group-writable homes, and legacy updaters (+3 security alerts) - #1543

Merged
DeusData merged 1 commit into
mainfrom
fix/batch1-strictness-and-installers
Aug 11, 2026
Merged

fix(install,daemon): unbreak npx clients, group-writable homes, and legacy updaters (+3 security alerts)#1543
DeusData merged 1 commit into
mainfrom
fix/batch1-strictness-and-installers

Conversation

@DeusData

Copy link
Copy Markdown
Owner

First batch of the post-v0.10.0 fix program. Five field reports in 24 hours all pointed at the same shape: gates that were right in principle refused ordinary setups, then failed to say why. Per the consolidated strictness decision, each keeps the protection that matters, drops the part refusing legitimate users, and every refusal now names what it refused and how to proceed.

Fixes

Issue Was Now
#1539 / #1383 every npx client rejected (image_unverifiable), dying after 30 s with zero bytes on stdout unverifiable-but-HELLO-proven peers admitted with a warn log; fingerprint mismatch still rejects hard; bootstrap failures emit a JSON-RPC error naming the reason
#1535 / disc. #1526 group-writable ancestor (WSL2/distro default ~/.local at 0775) blocked install, reported as activation transaction I/O failed ancestors warn, leaf stays owner-private, refusals name directory + mode + rule
#1538 / disc. #1526 update offered a ui/standard choice where "ui" could only 404 chooser and flags removed; release publishes ui-* alias assets so shipped 0.9.x updaters work again
#1537 benign xattr line mistaken for the failure; refusal blamed invisible sessions xattr silenced; refusal points at daemon status for the holding pids

Security alerts (all three open Scorecard findings)

  • HIGH — binary artifact: an 8.8 MB compiled Go ELF wrapper was committed by accident at pkg/go/codebase-memory-mcp. Removed + gitignored (with .exe sibling).
  • HIGH — GHSA-2v37-7h3g-55p8: nanoid < 3.3.17 infinite loop; transitive via postcss→vite, pinned through the existing overrides block (lockfile resolves 3.3.18) rather than promoted to a direct dep.
  • MEDIUM — unpinned pip: publish resolved its whole transitive graph at run time. pkg/pypi/requirements-publish.txt hash-pins the toolchain (316 hashes, generated on linux/amd64 python:3.12 to match the runner); step now uses --require-hashes. Verified by installing from it in that image.

Riders: hatchling pinned (the exact cause of the v0.10.1 publish break), SECURITY.md supported-versions → 0.10.x.

Verification

New 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. Local suites green (518 passed); full 3-OS ladder running. Two ASan stack-overflows surfaced during the work were my own test-edit argc mistakes — fixed, and every argc/array pair in test_cli.c audited.

Community PRs merged alongside: #1525 (anyOf schema — verified the runtime still rejects both-omitted), #1530 (tar --no-same-owner). #1512/#1514 are correct but on a stale base; #1513 closed as already-fixed.

Ships as v0.10.2.

@DeusData
DeusData force-pushed the fix/batch1-strictness-and-installers branch 2 times, most recently from 32fc5d1 to 99999da Compare August 11, 2026 11:24
…egacy 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>
@DeusData
DeusData force-pushed the fix/batch1-strictness-and-installers branch from 99999da to 8eabe19 Compare August 11, 2026 11:37
@DeusData
DeusData merged commit b377c62 into main Aug 11, 2026
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant