Files
better-claude-config/tests/test_catalog_console_packaging.py
BCC Agent f0d0ab7a08
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 23s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 12s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 10s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 10s
feat: Catalog Console -- maintainer-only review + signing tool (#62)
Adds a separate PySide6 tool (catalog_console.py) that reviews proposed
changes to data/catalog.json and signs the approved result. Never shipped
to users, never in the release bundle -- maintainer runs it from source.

Pure, GUI-free logic lives in a new catalog_review.py (kept out of both
the GUI and bcc_core.py to avoid merge conflicts on the latter):

- diff_catalogs(old, new) -> list[EntryChange]: semantic (per-entry)
  diff, not a text diff, with per-field before/after values.
- Six independent risk predicates, each unit-tested: non-empty
  env_required value, command outside bcc_core.CATALOG_ALLOWED_COMMANDS
  (imported, not redefined), non-ASCII code points in id/command/args
  (rendered with escapes -- homoglyph/RTL-override defence), unpinned
  npm/docker package references, URL domain changes (lookalike-domain
  swap defence), brand-new entries flagged for extra scrutiny.
- ReviewSession + can_sign(): the Sign button stays disabled until every
  changed entry is individually acknowledged -- no "acknowledge all"
  shortcut exists, and a comment in the code says never to add one.
- TOCTOU fix (adversarial review on #62): the git blob SHA of
  data/catalog.json is pinned when review begins; can_sign() refuses to
  sign if the current blob differs, forcing a re-review. The Console
  re-fetches the blob SHA immediately before signing and enforces this.
- catalog_signing_message() imports bcc_core's domain-separation prefix
  (_CATALOG_SIG_DOMAIN) rather than retyping it, so the Console's
  signatures and bcc_core.verify_catalog_signature can't drift apart --
  proven by a round-trip test (sign here, verify via bcc_core).
- encrypt_private_key/decrypt_private_key: the signing key is never
  stored plaintext (scrypt + AES-256-GCM at rest, OS keychain via the
  optional keyring package if available, else an encrypted file under
  $HOME outside the repo).
- Registry lookup (lookup_registry_info + injected Fetcher): the network
  call is kept out of this module for offline testability;
  catalog_console.py supplies npm/PyPI HTTP fetchers. Fails soft --
  network down means "unavailable", never a block on review.
  near_neighbor_ids() flags edit-distance <=2 typosquat candidates
  against existing catalog ids.

catalog_console.py wires the above into a Qt GUI: Load (open PRs
touching data/catalog.json via the Gitea REST API, or main) -> Review
(one EntryCard per changed entry, command/args rendered visually
dominant, risk findings colour-coded, per-card registry-lookup button
running off the UI thread like bcc.py's ConnTester/SpawnTester) -> Sign
(re-checks the pinned blob SHA, prompts for the key passphrase, writes
data/catalog.json + data/catalog.json.sig and commits+pushes BOTH in a
single commit -- so main is never red between a catalog merge and its
signature). Every attacker-controlled string renders through a
plain_label() helper that both escapes HTML and forces Qt.PlainText, so
a script/image payload in a description/notes/URL can't render as
markup. Also provides keygen (generates + stores an encrypted keypair,
prints the base64 public key) and show-seed-b64 (prints the base64
private seed for the RELEASE_SIGNING_KEY CI secret) CLI subcommands.

Excluded from the release bundle: bcc.spec's Analysis() only ever starts
from bcc.py, and tests/test_catalog_console_packaging.py asserts neither
new file is named anywhere in bcc.spec and that bcc.py never imports
either module.

Tests: 67 new (62 in test_catalog_review.py, 5 in
test_catalog_console_packaging.py) covering diff_catalogs, every risk
predicate individually, the acknowledge-gating + TOCTOU can_sign()
logic, the sign/verify round-trip against bcc_core, key encryption
(including wrong-passphrase and corrupted-blob rejection),
edit-distance/near-neighbour matching, and registry-lookup fail-soft
behaviour. Full suite: 321 passed, 1 pre-existing unrelated skip. ruff
check and ruff format --check both clean. catalog_console.py (Qt/GUI)
could not be executed in the sandbox this was developed in (no system
EGL/GL libraries available for PySide6) -- it was syntax-checked
(py_compile) and lint/format-checked but not smoke-tested; see PR body
for what AJ should verify.

Closes #62
2026-07-12 17:57:00 -04:00

67 lines
2.7 KiB
Python

"""Asserts the maintainer-only Catalog Console (catalog_console.py,
catalog_review.py) is never bundled into the release binary.
A signing/review tool shipping to end users would be an own-goal (issue
#62): it has no reason to run on a user's machine, and its presence would
be a confusing artefact of a build that's supposed to be a thin GUI over
mcpServers config editing."""
from __future__ import annotations
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
SPEC_PATH = REPO_ROOT / "bcc.spec"
_EXCLUDED_FILES = ("catalog_console.py", "catalog_review.py")
def test_spec_file_exists():
assert SPEC_PATH.exists()
def test_console_files_not_named_in_spec():
"""The spec text must never reference either maintainer-only module --
not as the Analysis entry point, not in datas, not anywhere."""
spec_text = SPEC_PATH.read_text(encoding="utf-8")
for filename in _EXCLUDED_FILES:
assert filename not in spec_text, (
f"{filename} must never be referenced by bcc.spec -- it is a "
"maintainer-only tool and must not ship to users."
)
def test_spec_analysis_entry_point_is_bcc_py_only():
"""PyInstaller's Analysis(...) call determines the dependency-scanned
entry point(s); it must be bcc.py alone."""
spec_text = SPEC_PATH.read_text(encoding="utf-8")
assert 'Analysis(\n ["bcc.py"],' in spec_text or 'Analysis(["bcc.py"]' in spec_text, (
"bcc.spec's Analysis(...) entry point changed shape -- re-verify by hand "
"that catalog_console.py / catalog_review.py are still excluded."
)
def test_console_modules_exist_but_are_standalone_top_level_files():
"""Sanity check the files this test is guarding actually exist as
top-level modules (not, say, silently moved into a package PyInstaller's
Analysis would still pick up as an implicit import of bcc.py)."""
for filename in _EXCLUDED_FILES:
assert (REPO_ROOT / filename).exists()
# bcc.py must not import them.
bcc_text = (REPO_ROOT / "bcc.py").read_text(encoding="utf-8")
module_name = filename.removesuffix(".py")
assert f"import {module_name}" not in bcc_text
assert f"from {module_name}" not in bcc_text
def test_requirements_files_do_not_reference_console_only_needs():
"""catalog_console.py's only import beyond the shipped stack is the
optional `keyring` package, which is intentionally NOT added as a hard
dependency anywhere a user install would pick it up."""
for req_file in ("requirements.txt", "requirements-dev.txt"):
path = REPO_ROOT / req_file
if not path.exists():
continue
text = path.read_text(encoding="utf-8").lower()
assert "keyring" not in text