Publish SHA-256 checksums for every release artifact and sign them with a
domain-separated Ed25519 signature. Skips signing (loudly) if the key secret
is absent rather than failing the release. README documents verification and
states the limit plainly: this proves the file is the one we published; it
does not remove Gatekeeper/SmartScreen warnings.
- Pin every basic-tier entry to an exact published version (npm @x.y.z,
uvx @x.y.z, docker :tag). Unpinned npx -y <pkg> means a package
compromised AFTER we ship auto-upgrades into every user; a pin bounds
supply-chain compromise to versions we actually reviewed.
- Drop firecrawl from the seed (19 entries). npm publish rights are held
solely by hello_sideguide/sideguide.dev, which has no visible
relationship to firecrawl.dev, while the package is presented as
official. Publisher identity we cannot tie to the vendor is exactly
what this catalog must not execute on a user's machine. Retained in the
research pool pending confirmation.
- postgres: ship --access-mode=restricted, not unrestricted. A curated
catalog must not default to handing an LLM write access to your DB.
- Add last_release (ISO date, from the live registry) so the UI can show
freshness; postgres-mcp and mcp-obsidian are both ~14mo stale.
Phase 1 of the MCP server catalog: pure, GUI-free core functions plus the
seed data/catalog.json (20 servers). No GUI wiring in this PR -- bcc.py is
untouched; a follow-up PR adds the picker dialog.
- load_catalog(): strict json.loads ONLY. The lenient repair pipeline
(repair_json_text / parse_pasted_json*) is never used on catalog bytes,
by design and by comment, so a signature always authenticates exactly
what gets parsed.
- validate_catalog(): rejects the whole file (not per-entry) on: bad
schema/version types, missing tier-appropriate fields (basic needs
config.command+args, link-only needs docs_url and no config), a
command allowlist (npx/uvx/docker/node/python/python3 only), -e/--eval/-c
denial for node/python, --privileged and root/$HOME volume-mount denial
for docker, non-empty env_required values (hard rejection -- secrets
never ship in the catalog), secret-looking args (reuses
_TOKEN_PREFIXES/_is_secret_value rather than reimplementing), non-https
URL fields, and non-ASCII code points in id/command/args (homoglyph
defence).
- verify_catalog_signature(): Ed25519 via the cryptography package,
domain-separated message (the literal prefix "bcc-catalog-v1|" + raw
bytes), accepts a match against any key in CATALOG_PUBKEYS
(rotation-ready), never raises.
- resolve_catalog(): picks the highest version among bundled/cached/remote
candidates that EACH independently pass verify + validate -- the bundled
catalog gets no implicit trust, closing the hole where an unsigned
payload merged to main would win on being local. Anti-rollback (never
regress below the best verified candidate already in hand) and
anti-freeze (reject a jump of more than 1000 versions) built in.
- catalog_entry_to_paste_json() / config_has_unfilled_placeholders(): small
pure helpers the future GUI dialog will use to feed a catalog pick into
the existing paste-import path and to gate Save on unfilled placeholder
tokens.
data/catalog.json: the provided 20-server seed, with a signed_at field
added at the top level (lives inside the signed payload once real signing
lands in #62). Wired into bcc.spec's PyInstaller datas so it bundles into
the frozen app.
Security requirements from the issue, and where they landed:
- Catalog bytes never touch the lenient JSON repair path -- enforced by
load_catalog()'s strict json.loads and a comment warning against wiring
it in later.
- env_required values are a hard rejection when non-empty, not a warning.
- Secret-looking args are rejected at validation time, reusing the
existing secret-detection helpers instead of duplicating them.
- Non-ASCII id/command/args rejected (typosquat/homoglyph defence).
- URL fields restricted to https://.
- Ed25519 signature verification is domain-separated and never raises.
- The bundled catalog is verified at runtime exactly like remote/cached --
no implicit trust for being local.
- Anti-rollback and anti-freeze bounds on resolve_catalog's version
comparison.
Tests: 42 new tests added to tests/test_core.py (full suite: 239 passed,
1 pre-existing unrelated skip). ruff check and ruff format --check both
clean.
Publish SHA256SUMS for every release archive and sign it with a
detached Ed25519 signature (SHA256SUMS.sig), since paid code signing
(macOS Developer ID, Windows Authenticode) and Sigstore keyless (needs
a Fulcio-trusted OIDC issuer; self-hosted Gitea isn't one) are both
out of budget/scope.
- scripts/sign_checksums.py: dependency-light (cryptography only)
helper to hash a directory of files into a sha256sum(1)-compatible
SHA256SUMS manifest, sign it (domain-separated: b"bcc-release-v1|"
+ raw manifest bytes), and verify a signature. CLI has generate/
sign/verify subcommands; verify doubles as the check path.
- tests/test_checksums.py: 15 unit + CLI-subprocess tests covering
hashing, manifest formatting, sign/verify roundtrip, tamper
detection, wrong-key rejection, domain-separation, and the
no-key-provided failure path (must error, never write an empty/
bogus .sig).
- .github/workflows/release.yml: Publish Release job now checks out
the repo, flattens build artifacts, generates SHA256SUMS, and signs
it from the RELEASE_SIGNING_KEY secret (base64 raw Ed25519 seed) if
present. If the secret is absent, the release still publishes with
a loud ::warning:: and no .sig — it never fails the release or
publishes a bogus signature.
- README.md: new 'Verifying your download' section with the (still
placeholder) public key, sha256sum -c / Get-FileHash commands, and
an explicit statement that this does not remove Gatekeeper/
SmartScreen warnings.
- requirements-dev.txt / ci.yml: add cryptography as a dev/test
dependency for the new script and its tests.
Touches no files from bcc_core.py / tests/test_core.py /
pyproject.toml / bcc.spec to avoid colliding with concurrent work on
those files.
Sets live in the config under _bccServerSets (bcc-owned, ignored by
Claude, travels with the file). Apply enables exactly the set's members
and parks the rest; vanished members are reported, not fatal. GUI row:
set combo + Apply + Save set… + delete.
Closes#52
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First release actually shipping the v1.2.0 feature set (the v1.2.0
tag's release run was cancelled and produced no assets) plus the
audit fixes #32-#40 and the Windows process-tree kill (#13).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Popen.kill() only terminated the direct child, so runner-style commands
(npx -> node -> server) leaked the real server process on every Windows
spawn test. taskkill /PID <pid> /T /F walks the descendant tree. Also
sets CREATE_NO_WINDOW on the spawned test process so the windowed exe
doesn't flash a console per test.
Closes#13
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
python3 is not a command on a stock Windows install; caught by the new
windows-latest CI job. Probe 'python' there and accept warn (found on
augmented PATH) as proof of resolution.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The status bar is rewritten on every action, so the appended MSIX
warning vanished on first interaction. A dedicated warn banner under
the top bar stays visible.
Closes#35
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The self-hosted Windows runner's PowerShell execution policy rejects
setup-python's install script, so Windows mirrors release.yml: py -3.12
+ venv (the version the release binaries ship with). Linux keeps the
full 3.10/3.12/3.13 setup-python matrix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Linux (and any non-desktop platform): refuse instead of 'pkill claude',
which substring-matched running Claude Code CLI sessions and relaunched
the CLI, not a desktop app. New restart_supported() gates the button.
- macOS: wait (<=5s) for the old instance to exit before 'open -a Claude'
so the relaunch can't re-activate the dying process. Runs off the UI
thread via a RestartWorker.
- Windows: verify the Start-menu shortcut exists BEFORE taskkill, so an
MSIX/Store install is never killed without a relaunch path.
Closes#33
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closing the About dialog mid-check GC'd the dialog and the running
QThread with it -> 'QThread: Destroyed while thread is still running'.
A class-level keepalive set now holds each worker until finished;
stale results to a destroyed receiver are dropped by Qt.
Closes#32
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>