Reference in New Issue
Block a user
Delete Branch "feat/10-catalog-core"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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.pyis untouched; a follow-up PR adds the picker dialog. Follows the existing house style: pure/unit-tested logic inbcc_core.py.What's in
bcc_core.pyload_catalog(raw)-- strictjson.loadsONLY. The lenient repair pipeline (repair_json_text/parse_pasted_json*) is never used on catalog bytes, by design and by an explicit comment warning future editors not to wire it in: a signature only means something if it authenticates the exact bytes that get parsed.validate_catalog(data) -> list[str]-- rejects the whole file (not just the bad entry) on: missing/malformedschema/version/servers; missing tier-appropriate fields (basic needsconfig.command+args; link-only needsdocs_urland must NOT haveconfig); a command allowlist (npx/uvx/docker/node/python/python3only);-e/--eval/-cdenial for node/python;--privilegedand/- or$HOME-rooted volume-mount denial for docker; non-emptyenv_requiredvalues as a hard rejection (never a warning -- secrets never ship in the catalog); secret-looking args (reuses the existing_TOKEN_PREFIXES/_is_secret_valuerather than reimplementing); non-https://URL fields (homepage/docs_url/source); and non-ASCII code points inid/command/args (homoglyph/typosquat defence).catalog_version(data) -> intverify_catalog_signature(raw, sig, pubkeys) -> bool-- Ed25519 via thecryptographypackage (added topyproject.toml). Domain-separated: the signed message is the literal prefix"bcc-catalog-v1|"+ raw bytes, not raw alone. Returns True if ANY key inpubkeysverifies (rotation-ready). Never raises.CATALOG_PUBKEYS: list[bytes]-- module constant seeded with a placeholder +# TODO: real key from Catalog Console (#62); tests generate their own keypairs.resolve_catalog(bundled, cached, remote) -> dict-- picks the highest version among candidates that each independently pass verify + validate. The bundled catalog gets no implicit trust -- it's verified at runtime exactly like remote/cached, closing the hole where an unsigned payload merged to main would win the version comparison just by being local. Anti-rollback (never regress below the best verified candidate already in hand this pass) and anti-freeze (reject a jump of more than 1000 versions) are both enforced.catalog_entry_to_paste_json(entry) -> dict-- basic-tier entry to{name: {command, args, env}}, feeding the existing_import_serverpath.config_has_unfilled_placeholders(cfg) -> bool-- true if any<PLACEHOLDER>token remains, for the future GUI to gate Save on.Data
data/catalog.json: the provided 20-server seed, with asigned_atfield added at the top level (lives inside the signed payload once real signing lands in #62).bcc.spec'sdatasnow bundlesdata/catalog.jsoninto the frozen app (same mechanism #20 added for icons).Security requirements from the issue -- where they landed
load_catalog()'s strictjson.loadsplus a comment warning against wiring it in later. ✅env_requirednon-empty values are a hard rejection, not a warning. ✅id/command/args rejected (typosquat/homoglyph defence). ✅https://. ✅bcc-catalog-v1|prefix) and never raises. ✅resolve_catalog's version comparison. ✅Tests
42 new tests added to
tests/test_core.py, covering: valid round-trip; tampered-byte signature failure; wrong-key failure; garbage/empty/missing signature; each validator rule's rejection (disallowed command,node -e,python3 -c,docker --privileged, docker/+$HOMEvolume mounts, non-emptyenv_required, secret-looking arg incl. token-prefix,http:///file://URLs, non-ASCII id/command/arg, duplicate ids); malformed JSON handling; the real shippeddata/catalog.jsonpassingvalidate_catalog()(the regression test that matters most); andresolve_catalogpreferring the highest verified version while rejecting an unsigned bundled catalog, a rolled-back version, and an absurd version jump.Full suite: 239 passed, 1 pre-existing unrelated skip (Windows-only test).
ruff checkandruff format --checkboth clean.Deviations from the brief
resolve_catalog's exact parameter shape wasn't fully specified beyond(bundled, cached, remote) -> dict. I made each argument eitherNoneor a(raw_bytes, signature_bytes)tuple, verifying+validating from raw bytes inside the function (using the module-levelCATALOG_PUBKEYS), and return{}when nothing verifies -- this seemed like the most literal reading that still lets the function independently re-verify every candidate includingbundled.