Commit Graph

130 Commits

Author SHA1 Message Date
the_og febd617c56 feat: light theme + system-following, with the dark theme preserved exactly
CI / Lint (ruff) (pull_request) Successful in 8s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 12s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 10s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 11s
CI / Catalog signature (pull_request) Successful in 6s
CI / Tests (py3.12 / windows-latest) (pull_request) Has been cancelled
BCC has always been dark-only -- BG #1b1d23, hardcoded at import time, with
no light option and no awareness of the desktop's appearance. On a light
desktop it matches nothing else on screen and there was no way to change it.

Adds a Palette value type in bcc_core with DARK (byte-identical to the
colours v1.3.0 shipped) and a new LIGHT, plus resolve_theme(setting,
system_is_dark) so the decision is testable without a Qt app. View > Theme
offers Match system / Light / Dark, persisted in QSettings under ui/theme,
defaulting to following the system.

The light palette's semantic colours are deliberately not the dark ones
lightened: #4ade80 sits near 1.7:1 against white. They are darkened to clear
WCAG AA, and a contrast test enforces >= 4.5:1 for every text colour against
its surface in both palettes so nobody harmonises them back later.

Three near-black literals were baked into the stylesheet (#1a1205 on-accent
text, #202229 disabled table, #16181d diagnostics pane). Fine with one theme,
invisible breakage with two -- each now has a palette slot, and a test
asserts build_stylesheet contains no hex literals at all.

The ~20 inline setStyleSheet(f"color: {MUTED}") call sites are left alone:
apply_palette rebinds the module-level colour names, and an f-string resolves
its names when it runs, so each call site picks up the new colour on its next
render. Switching theme reapplies the global QSS and re-renders the
inline-styled widgets, so nothing is left dark-on-light.

Refs #75
2026-07-20 12:22:38 -04:00
the_og cd2ac2f6f8 Merge PR #69: make the review gate load-bearing, split the keys (#68)
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 10s
CI / Tests (py3.12 / windows-latest) (push) Successful in 24s
CI / Lint (ruff) (push) Successful in 6s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 11s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 11s
CI / Catalog signature (push) Successful in 7s
Finding 1: can_sign() returned True on an empty changeset, and _on_sign
compared the reviewed blob against a hardcoded "main" rather than the ref
actually reviewed — so the PR path could never sign, and the main-vs-main
path unlocked Sign with zero entries acknowledged. That is how commit b08cf21
signed 19 entries nobody reviewed. can_sign now refuses an empty diff, checks
has_blocking_risk itself instead of trusting a GUI checkbox, and resolves the
TOCTOU pin from the reviewed ref via a pure, testable sign_precondition().

Finding 5: the catalog key and the release key are now separate. The release
key lives in CI and signs checksums; the catalog key stays offline and signs
what users execute. A CI compromise gets the former, not the latter.
2026-07-12 21:30:13 -04:00
the_og 26c66b7db1 Merge branch 'main' into fix/68-console-gate
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 24s
CI / Lint (ruff) (pull_request) Successful in 6s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 10s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 10s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 10s
CI / Catalog signature (pull_request) Successful in 6s
2026-07-12 21:28:27 -04:00
the_og 86139100eb Merge PR #70: enforce the checks we said we had (#68)
CI / Tests (py3.12 / windows-latest) (push) Successful in 23s
CI / Lint (ruff) (push) Successful in 6s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 11s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 10s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 10s
CI / Catalog signature (push) Successful in 6s
Findings 2/3/4/6/7. config.env now gets the deny-list, ASCII, secret and
empty-or-placeholder checks that args always had; version pinning is enforced
at runtime, not only in the maintainer tool; the CI gate pins the expected
pubkey instead of trusting the one in the PR it is reviewing; resolve_catalog
anchors its cap to the bundled version and prefers bundled on ties; catalog
ids are constrained.

Tests rewritten: the old fixtures asserted the unpinned form validates clean
and that config.env passes through verbatim — they enshrined two of the bugs.
2026-07-12 21:28:24 -04:00
BCC Agent 38f14deeff fix(core): validate config.env, enforce version pinning, fix CI trust anchor and resolve_catalog guards (#68)
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 11s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 11s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 10s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 23s
CI / Catalog signature (pull_request) Successful in 7s
Fixes findings 2, 3, 4, 6, 7 from the issue #68 adversarial review.

- Finding 2: config.env was type-checked only. Add CATALOG_DENIED_ENV_KEYS
  (case-insensitive) for interpreter/loader-override keys (NODE_OPTIONS,
  PYTHONPATH, LD_PRELOAD, ...), apply the ASCII check and the existing
  secret-value check to env keys/values, and require env values to be
  empty or a single <PLACEHOLDER> token.

- Finding 3: version pinning was only checked by catalog_review.py (which
  never runs on the signing path per finding 1). Move enforcement into
  _validate_catalog_config: npm/uvx specs must carry @version or ==version
  (scoped names handled), docker images must have an explicit non-latest
  tag. Only the first plausible package-spec token is checked, so flags,
  <PLACEHOLDER>s, and docker subcommands/flags don't trip it. All 19 real
  catalog entries still validate clean.

- Finding 4: the CI catalog-signature gate imported bcc_core from the PR
  branch and trusted whatever CATALOG_PUBKEYS said there, so a PR changing
  both catalog.json and CATALOG_PUBKEYS (with a matching signature) went
  green. ci.yml now hardcodes the expected base64 pubkey and asserts
  bcc_core.CATALOG_PUBKEYS matches it before verifying the signature.
  NOTE: the maintainer is planning to rotate this key -- update
  EXPECTED_CATALOG_PUBKEY_B64 in ci.yml as its own reviewed change when
  that happens, never bundled with a catalog content change.

- Finding 6: resolve_catalog's anti-rollback/anti-freeze guards sat behind
  `if best_version >= 0`, so the first verified candidate was accepted
  unconditionally and the anti-freeze anchor drifted with each accepted
  candidate instead of staying fixed. The cap is now measured against the
  bundled catalog's version specifically (the trust anchor baked into the
  binary), regardless of evaluation order; bundled wins version ties; and
  a new pure `floor` parameter lets a future caller pass a persisted
  accepted-version floor.

- Finding 7: catalog id is now constrained to ^[a-z0-9][a-z0-9._-]{0,63}$.

Tests: fixed _minimal_catalog to use a pinned package (was enshrining
finding 3), rewrote the env-passthrough test to prove the validation
boundary instead of asserting env passes through unchecked, and
reordered test_resolve_catalog_rejects_absurd_version_jump so it
actually exercises the first-candidate path. Added positive/negative
tests for every new rule. Manually verified each new check by commenting
it out and confirming the guarding test goes red, then restoring it.
2026-07-12 21:27:08 -04:00
BCC Fix Agent 82483e693d fix(catalog-console): close the vacuous review gate; split catalog/release signing keys
CI / Lint (ruff) (pull_request) Successful in 9s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 11s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 34s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 9s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 10s
CI / Catalog signature (pull_request) Successful in 6s
Fixes #68 findings 1 and 5.

Finding 1 -- the review gate signed without reviewing anything:
- ReviewWindow._on_sign hardcoded "main" as the TOCTOU comparison ref, so
  any PR review (where _on_load pins the PR head's blob SHA) could never
  sign; the only working path was main-vs-itself, whose empty diff made
  can_sign() vacuously True (set() <= set()). Commit b08cf21 signed 19
  entries through exactly that path with zero of them reviewed.
- can_sign() now refuses an empty changeset outright, and itself checks
  has_blocking_risk() across every changed entry rather than trusting the
  GUI to have disabled a checkbox.
- ReviewSession now carries loaded_ref (the exact ref reviewed); a new pure
  sign_precondition(session, resolve_blob_sha) resolves the TOCTOU SHA from
  that ref, never a hardcoded "main". _on_load's retry path re-diffs
  instead of re-pinning the same stale SHA, so a blob-mismatch refusal
  can't loop forever.
- source="main" now diffs against the last catalog a maintainer actually
  SIGNED (walking catalog.json's git history until a version verifies
  against the current .sig), not against itself.
- Replaced the theatre-only test_no_acknowledge_all_function_exists (only
  asserted no function was *named* acknowledge_all) with a test that also
  exercises the real gate. Added can_sign/sign_precondition coverage for
  the empty-diff, blocking-risk, and ref-resolution seams -- each verified
  to fail when its guard is removed.

Finding 5 -- the catalog key and release key were the same CI-resident key:
- scripts/sign_checksums.py gets its own RELEASE_PUBKEYS (separate from
  bcc_core.CATALOG_PUBKEYS) and a verify_checksums_against_any() helper.
- release.yml's signing-smoke-test now verifies RELEASE_SIGNING_KEY against
  RELEASE_PUBKEYS only -- it no longer imports bcc_core/CATALOG_PUBKEYS at
  all, so this workflow can never compare a CI secret against the
  catalog's root of trust.
- catalog_console.py: keygen/show-seed-b64 gain --release, with separate
  keychain/file storage per key kind. show-seed-b64 refuses to run without
  --release, so the catalog seed can't be exported to a CI secret by habit.
- README documents both keys' trust properties and the asymmetry: a CI
  compromise burns the release key, never the catalog key.

The maintainer must rotate the catalog key (it was CI-resident, so treat it
as burned for catalog use) and generate a fresh release key -- see the PR
description for the exact steps. No key is generated or committed here.
2026-07-12 21:25:34 -04:00
the_og 6fce19cc67 ci: gate the catalog signature, smoke-test the release key (#61, #63)
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 10s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 10s
CI / Lint (ruff) (push) Successful in 6s
CI / Tests (py3.12 / windows-latest) (push) Successful in 23s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 10s
CI / Catalog signature (push) Successful in 6s
Two gaps closed now that a real key exists.

1. CI 'Catalog signature' job (the #61 gate): every push/PR verifies
   data/catalog.json against data/catalog.json.sig using the public key in
   bcc_core, and runs validate_catalog. The threat model here is not an
   outsider pushing to the repo -- it is merging a friendly-looking PR
   without really reading it. A contributor can change catalog.json but
   cannot produce a matching signature, so a blindly-merged PR now lands as
   a red build within a minute instead of quietly riding into the next
   release. Public-key only; no secret involved.

2. release.yml 'Signing key smoke test' (workflow_dispatch only): the
   Publish job is gated on a tag, so a manual run never exercised signing --
   a wrong or missing RELEASE_SIGNING_KEY would first surface during a real
   release. This signs a throwaway manifest with the secret and verifies it
   against the public key compiled into bcc_core, proving the two halves of
   the keypair actually match. Publishes nothing.
2026-07-12 18:30:30 -04:00
the_og 37b3c8f5d0 catalog: trust the real signing key
CI / Tests (py3.12 / windows-latest) (push) Successful in 23s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 10s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 10s
CI / Lint (ruff) (push) Successful in 6s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 9s
Adds the Ed25519 public key generated by the Catalog Console (#62),
replacing the b"\x00"*32 placeholder, and imports base64 (the key line
referenced it without the import, so bcc_core failed to load at all).

Verified end to end against the signature the Console pushed in b08cf21:
signature verifies, catalog validates clean, resolve_catalog accepts the
bundled copy (19 servers), and a single-byte tamper is rejected.
2026-07-12 18:25:49 -04:00
Cowork Supervisor b08cf2112b chore: sign data/catalog.json (Catalog Console, #62)
CI / Lint (ruff) (push) Successful in 8s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 10s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 10s
CI / Tests (py3.12 / windows-latest) (push) Successful in 22s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 10s
Payload and detached Ed25519 signature land together so main is never red between a catalog merge and its signature.
2026-07-12 18:22:41 -04:00
the_og 80761a1f17 Merge PR #66: Catalog Console — maintainer review + signing tool (#62)
CI / Tests (py3.12 / windows-latest) (push) Successful in 22s
CI / Lint (ruff) (push) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 10s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 11s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 12s
Maintainer-only PySide6 tool: semantic per-entry diff of data/catalog.json,
blocking risk predicates (non-empty env values, command allowlist, non-ASCII
homoglyphs, unpinned packages, URL domain changes), automatic off-thread
registry lookups (publisher/age/downloads/near-neighbour), acknowledge-gated
signing with a pinned git blob SHA (refuses to sign bytes that changed since
review), and payload+signature emitted in a single commit.

Never shipped to users — excluded from bcc.spec, with a test asserting it.
2026-07-12 18:04:53 -04:00
BCC Agent d6fc6845c4 fix(catalog-console): run registry lookup automatically, not on click (#62)
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 10s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 22s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 11s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 10s
Review feedback: making the registry lookup an on-demand 'Check registry'
button was a deviation from the design intent, not a style choice. The
registry lookup is the one check a human reviewer genuinely cannot do by
eye -- it's what caught firecrawl-mcp's unrelated npm publisher in the
seed data. Gating it behind a button makes it optional, and an optional
check is the one a tired maintainer skips at 11pm -- exactly the failure
mode this tool exists to defend against. The friction belongs on
approval, never on information.

EntryCard now kicks off its registry lookup automatically at construction
time (i.e. as soon as _render_cards() builds the cards for a loaded
review), one RegistryLookupWorker (QThread) per changed entry, all
starting concurrently as the cards are built. Nothing about the lookup's
pure logic changed -- catalog_review.lookup_registry_info was already
fail-soft (RegistryInfo(available=False) on any fetch problem, never an
exception) and can_sign() never depended on registry state, so a dead
registry still cannot gate review or signing.

Renamed the button 'Check registry' -> 'Re-check' and kept it wired to
the same _run_registry_lookup(), for manually retrying a failed/unavailable
lookup. Label copy now reads loading... while a lookup is in flight (was
'not checked yet.' / 'checking...'), matching the loading -> result |
unavailable per-entry states.

No change to acknowledge-gating or the TOCTOU blob-SHA pin in
catalog_review.py. ruff check/format clean; full suite still 321 passed,
1 pre-existing unrelated skip -- catalog_review.py (the tested pure-logic
module) is untouched, only catalog_console.py's GUI wiring moved from
button-triggered to auto-triggered.
2026-07-12 18:04:28 -04:00
BCC Agent f0d0ab7a08 feat: Catalog Console -- maintainer-only review + signing tool (#62)
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
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
the_og 3841106630 Merge pull request 'feat: MCP server catalog core -- signed, validated, resolvable (#10, #61)' (#65) from feat/10-catalog-core into main
CI / Lint (ruff) (push) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 9s
CI / Tests (py3.12 / windows-latest) (push) Successful in 22s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 8s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 9s
2026-07-12 17:55:49 -04:00
the_og e3581b6e8b Merge branch 'main' into feat/10-catalog-core
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 10s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 11s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 43s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 10s
2026-07-12 17:42:12 -04:00
the_og 672d78f903 Merge PR #64: signed SHA256SUMS for releases (#63)
CI / Lint (ruff) (push) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 10s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 10s
CI / Tests (py3.12 / windows-latest) (push) Successful in 41s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 11s
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.
2026-07-12 17:42:09 -04:00
the_og 88e93edc6c catalog: pin exact package versions, drop firecrawl, add last_release
CI / Tests (py3.12 / windows-latest) (pull_request) Failing after 16s
CI / Lint (ruff) (pull_request) Successful in 52s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 12s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 14s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 14s
- 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.
2026-07-12 17:35:48 -04:00
Cowork Agent 48904c7787 feat: MCP server catalog core -- signed, validated, resolvable (#10, #61)
CI / Lint (ruff) (pull_request) Successful in 12s
CI / Tests (py3.12 / windows-latest) (pull_request) Failing after 17s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 10s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 14s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 39s
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.
2026-07-12 17:32:50 -04:00
BCC Agent cd38fd0c78 Sign release checksums with Ed25519 (#63)
CI / Lint (ruff) (pull_request) Successful in 6s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 12s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 23s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 11s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 13s
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.
2026-07-12 17:30:09 -04:00
the_og e6b60e94e7 Merge pull request 'release: v1.3.0 — named server sets, project config discovery, schema lint, UX polish' (#60) from release/v1.3.0 into main
CI / Lint (ruff) (push) Successful in 7s
CI / Tests (py3.12 / windows-latest) (push) Successful in 20s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 9s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 9s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 9s
Build & Release / Build (Windows) (push) Successful in 53s
Build & Release / Build (Linux) (push) Successful in 1m0s
Build & Release / Build (macOS) (push) Successful in 1m58s
Build & Release / Publish Release (push) Successful in 10s
v1.3.0
2026-07-12 14:03:50 -04:00
Cowork Supervisor 4afe21666d release: bump version to 1.3.0
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 21s
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 10s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 9s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 9s
Named server sets (#52), project .mcp.json discovery (#53), schema
lint (#54), Ctrl+S + enable/disable-all (#55).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 14:02:38 -04:00
the_og 06e74d4d2c Merge pull request 'feat: Ctrl+S save shortcut + enable/disable-all actions (#55)' (#59) from feat/55-ux-polish into main
CI / Lint (ruff) (push) Successful in 6s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 10s
CI / Tests (py3.12 / windows-latest) (push) Successful in 20s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 9s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 9s
2026-07-12 14:02:23 -04:00
Cowork Supervisor f92b851127 Merge remote-tracking branch 'origin/main' into feat/55-ux-polish
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 9s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 20s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 9s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 9s
2026-07-12 14:00:56 -04:00
the_og 47c95ac006 Merge pull request 'feat: named server sets — save/apply the Active/Disabled split (#52)' (#56) from feat/52-server-sets into main
CI / Lint (ruff) (push) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 10s
CI / Tests (py3.12 / windows-latest) (push) Successful in 20s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 9s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 9s
2026-07-12 14:00:19 -04:00
the_og 6b22ad26f0 Merge pull request 'feat: structural schema lint for server definitions (#54)' (#58) from feat/54-schema-lint into main
CI / Lint (ruff) (push) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 9s
CI / Tests (py3.12 / windows-latest) (push) Successful in 20s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 9s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 9s
2026-07-12 14:00:12 -04:00
the_og 874948506c Merge pull request 'feat: discover Claude Code project .mcp.json configs as profiles (#53)' (#57) from feat/53-project-mcp-discovery into main
CI / Lint (ruff) (push) Successful in 7s
CI / Tests (py3.12 / windows-latest) (push) Successful in 20s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 8s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 8s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 9s
2026-07-12 13:58:52 -04:00
Cowork Supervisor ac2e73e9d7 feat: named server sets — save/apply the Active/Disabled split (#52)
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 8s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 21s
CI / Lint (ruff) (pull_request) Successful in 6s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 9s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 8s
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>
2026-07-12 13:55:36 -04:00
Cowork Supervisor 82ff149373 feat: structural schema lint for server definitions (#54)
CI / Lint (ruff) (pull_request) Successful in 6s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 8s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 19s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 9s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 8s
Closes #54

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 13:55:22 -04:00
Cowork Supervisor 31ef4a0e85 feat: Ctrl+S save shortcut + enable/disable-all actions (#55)
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 20s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 8s
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 9s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 8s
Closes #55

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 13:55:19 -04:00
Cowork Supervisor 520b1b2ffd feat: discover Claude Code project .mcp.json configs as profiles (#53)
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 9s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 9s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 20s
CI / Lint (ruff) (pull_request) Successful in 6s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 9s
Closes #53

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 13:53:38 -04:00
the_og 9f535fb77f Merge pull request 'release: v1.2.1 — first shipped build with icon fix, audit fixes #32–#40, Windows tree-kill (#13)' (#51) from release/v1.2.1 into main
CI / Lint (ruff) (push) Successful in 9s
Build & Release / Build (Linux) (push) Successful in 1m0s
Build & Release / Build (Windows) (push) Successful in 58s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 9s
CI / Tests (py3.12 / windows-latest) (push) Successful in 20s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 8s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 8s
Build & Release / Build (macOS) (push) Successful in 1m43s
Build & Release / Publish Release (push) Successful in 9s
v1.2.1
2026-07-12 13:33:26 -04:00
Cowork Supervisor 8cf19d43c4 test: version-sync test reads pyproject.toml instead of hard-coding
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 8s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 19s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 8s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 8s
CI / Lint (ruff) (pull_request) Successful in 7s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 13:32:23 -04:00
Cowork Supervisor 0ef4586698 release: bump version to 1.2.1
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>
2026-07-12 13:31:36 -04:00
the_og ed7c40cac9 Merge pull request 'fix: kill the whole process tree on Windows spawn-test timeout (#13)' (#50) from fix/13-windows-tree-kill into main
CI / Lint (ruff) (push) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 9s
CI / Tests (py3.12 / windows-latest) (push) Successful in 21s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 9s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 9s
2026-07-12 13:31:15 -04:00
Cowork Supervisor 1384ed9703 fix: kill the whole process tree on Windows spawn-test timeout (#13)
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 8s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 20s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 9s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 8s
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>
2026-07-12 13:30:02 -04:00
the_og 41891ddad4 Merge pull request 'fix: block duplicate env/header keys at entry instead of silently dropping one (#38)' (#49) from fix/38-duplicate-kv-keys into main
CI / Tests (py3.12 / windows-latest) (push) Successful in 18s
CI / Lint (ruff) (push) Successful in 13s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 12s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 13s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 12s
2026-07-12 13:06:54 -04:00
the_og 408f517c5d Merge pull request 'fix: pop old AND new name from health cache on rename (#36)' (#48) from fix/36-health-cache-rename into main
CI / Tests (py3.12 / windows-latest) (push) Successful in 17s
CI / Lint (ruff) (push) Successful in 11s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 14s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 11s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 13s
2026-07-12 13:06:48 -04:00
the_og 9036729cd8 Merge pull request 'fix: MSIX warning gets a persistent banner instead of the status bar (#35)' (#47) from fix/35-msix-banner into main
CI / Tests (py3.12 / windows-latest) (push) Successful in 18s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 14s
CI / Lint (ruff) (push) Successful in 9s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 12s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 14s
2026-07-12 13:06:41 -04:00
the_og 29a08e9532 Merge pull request 'ci: test on windows + python 3.13 (#40)' (#46) from ci/40-test-matrix into main
CI / Tests (py3.12 / windows-latest) (push) Successful in 18s
CI / Lint (ruff) (push) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 10s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 12s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 10s
2026-07-12 13:06:34 -04:00
Cowork Supervisor 87303809b8 test: dep-check probe command must exist on Windows too
CI / Lint (ruff) (pull_request) Successful in 6s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 17s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 8s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 8s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 8s
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>
2026-07-12 13:05:04 -04:00
Cowork Supervisor 42456f25d2 fix: block duplicate env/header keys at entry instead of silently dropping one (#38)
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10) (pull_request) Successful in 9s
CI / Tests (py3.12) (pull_request) Successful in 9s
Closes #38

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 13:04:49 -04:00
Cowork Supervisor 2d274b9e03 fix: MSIX warning gets a persistent banner instead of the status bar (#35)
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10) (pull_request) Successful in 10s
CI / Tests (py3.12) (pull_request) Successful in 9s
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>
2026-07-12 13:03:57 -04:00
Cowork Supervisor 5c476bb13f fix: pop old AND new name from health cache on rename (#36)
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10) (pull_request) Successful in 9s
CI / Tests (py3.12) (pull_request) Successful in 10s
Closes #36

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 13:03:29 -04:00
Cowork Supervisor 62c8a2ea65 ci: use host py launcher for Windows tests (runner blocks setup-python)
CI / Tests (py3.12 / windows-latest) (pull_request) Failing after 20s
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 9s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 8s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 8s
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>
2026-07-12 13:02:09 -04:00
the_og b485357cd5 Merge pull request 'fix: import every dropped .json (aggregate counts) + 5 MB size guard (#39)' (#45) from fix/39-drop-import-all into main
CI / Lint (ruff) (push) Successful in 7s
CI / Tests (py3.10) (push) Successful in 9s
CI / Tests (py3.12) (push) Successful in 9s
2026-07-12 13:00:43 -04:00
the_og 6a91f830dc Merge pull request 'fix: only advance update/lastCheck after a completed check (#37)' (#44) from fix/37-update-lastcheck into main
CI / Lint (ruff) (push) Successful in 7s
CI / Tests (py3.10) (push) Successful in 9s
CI / Tests (py3.12) (push) Successful in 10s
2026-07-12 13:00:36 -04:00
the_og 6dacc26057 Merge pull request 'fix: harden restart_claude_desktop — Linux refusal, macOS quit-wait, Windows MSIX guard (#33)' (#43) from fix/33-restart-hardening into main
CI / Lint (ruff) (push) Successful in 7s
CI / Tests (py3.10) (push) Successful in 9s
CI / Tests (py3.12) (push) Successful in 9s
2026-07-12 13:00:30 -04:00
the_og 1087fc84d1 Merge pull request 'fix: keep UpdateCheckWorker alive until its thread finishes (#32)' (#42) from fix/32-about-worker-lifetime into main
CI / Lint (ruff) (push) Successful in 7s
CI / Tests (py3.10) (push) Successful in 8s
CI / Tests (py3.12) (push) Successful in 8s
2026-07-12 13:00:22 -04:00
the_og 5df364fb2e Merge pull request 'fix: spawn_test never raises — str-coerce env/command, wrap unexpected errors (#34)' (#41) from fix/34-spawn-test-env into main
CI / Lint (ruff) (push) Successful in 8s
CI / Tests (py3.10) (push) Successful in 8s
CI / Tests (py3.12) (push) Successful in 8s
2026-07-12 13:00:14 -04:00
Cowork Supervisor c7b2c90518 fix: spawn_test never raises — coerce env/command to str, wrap unexpected errors (#34)
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10) (pull_request) Successful in 8s
CI / Tests (py3.12) (pull_request) Successful in 8s
Closes #34

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 12:59:19 -04:00
Cowork Supervisor c493aa0c84 fix: harden restart_claude_desktop across platforms (#33)
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10) (pull_request) Successful in 9s
CI / Tests (py3.12) (pull_request) Successful in 10s
- 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>
2026-07-12 12:58:15 -04:00