Catalog Console: maintainer-only review + signing tool (no manual scripts) #62
Reference in New Issue
Block a user
Delete Branch "%!s()"
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?
Why
#61 makes the remote catalog trustworthy by requiring a maintainer signature. But if signing is a CLI script (
python scripts/sign_catalog.py), the security property quietly erodes: the path of least resistance becomes "merge PR, run script, push" without ever really reading the diff. A signing step you perform reflexively is not an approval — it's a rubber stamp with a key attached.The signature should be the artefact of an actual review, not a step that follows one. So: a small maintainer-only desktop tool where reviewing and signing are the same action.
What it is
bcc-catalog-console— a separate PySide6 app in this repo (never shipped to users, never in the release bundle; maintainer runs it from source).Flow:
data/catalog.json(Gitea API), plus the working state ofmain.commandandargschanges highlighted hard (this is the field that executes)env_requiredgaining a non-empty value → blocking error (values must always be empty)commandoutside the allowlist → blocking errorhomepage,docs_url,source→ shown with the domain broken out, so a swap fromgithub.com/foo/barto a lookalike domain is visiblevalidate_catalog()the app uses (imported frombcc_core, not reimplemented — one source of truth, per the recurring lesson from the masking work).data/catalog.json.sig, commits and pushes.Deliberate friction: the Sign button stays disabled until each changed entry is individually acknowledged. That is the whole point of the tool. Resist any urge to add a "sign all" shortcut later.
Tasks
diff_catalogs(old, new) -> list[EntryChange]— inbcc_core(or a shared module), unit-tested. The GUI stays thin.catalog.json.sig(reuse the existing tokened-remote flow).bcc.spec/ the release bundle. Add a test that asserts it is not in the packaged datas — a maintainer signing tool shipping to end users would be an own-goal.Non-goals
data/catalog.json.main.Related: #61 (verification side), #10 (catalog).
Adversarial review (Fable) — amendments to the Console
🔴 Review/sign TOCTOU
The Console reviews PR diffs but signs a file. Between "maintainer acknowledged every entry in PR #12" and "Sign pressed," the bytes on main can change — a second commit pushed to the same PR after review started, a force-push, a different PR merged in between. The signature must be cryptographically bound to the reviewed bytes, not to "whatever
data/catalog.jsonhappens to be now."Fix: pin the git blob SHA when review begins; sign exactly those bytes; refuse to sign if the current blob differs (re-review required). Cheap, and it's what makes "signing is the approval act" actually true rather than aspirational.
🔴 Emit payload + signature as ONE commit
Per the review on #61: if signing happens after merge, main is red between every catalog merge and the signing commit, and "red main" becomes routine noise — which is precisely the alarm-fatigue failure this whole design exists to prevent. The Console's output must be a single commit containing
catalog.json+catalog.json.sigtogether, or published state lives on apublishedbranch while main carries proposals. Unsigned catalog changes must not be able to land on the published surface at all.🟠 The Console renders attacker-controlled strings
description,notes,homepage,docs_url, package names inargs— all attacker-supplied, all rendered in the diff view.QLabelauto-interprets HTML. SetQt.PlainTexteverywhere and escape everything.@modelcontextprotocol/server-filesystemand a typosquat visually identical in a diff — defeating the entire point of the review tool. Rendercommand,argsand package names with non-ASCII code points explicitly flagged (highlight, or show escaped). A review tool that can be fooled by a lookalike character is worse than no review tool, because it manufactures confidence.🟠 Add the one check a human genuinely can't do
The dominant real attack is package-name substitution (typosquat / dependency confusion) in
args— it passes signature, schema, allowlist, and a tired human. Humans are bad at this and tools are good at it:Resolve each package name against the live registry (npm/PyPI) and show, next to the diff: publisher, package age, weekly downloads, and whether the name is a near-neighbour of an existing entry. Also enforce exact version pinning (
@scope/pkg@1.2.3) so a later-compromised package can't auto-upgrade into every user.This is where the actual security of the whole feature lives — not in the allowlist, not in "the user sees the command."
Updated task list
publishedbranch)Qt.PlainText+ full escaping on every attacker-controlled stringcommand/args/ package names (homoglyph defence)argsvalidate_catalog()frombcc_core— never reimplement (one source of truth)