main
172
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f73b49f17c |
Merge pull request 'release: prep 1.4.0 — bump version + finalize changelog' (#109) from release/v1.4.0 into main
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 13s
CI / Lint (ruff) (push) Successful in 8s
CI / Tests (py3.12 / windows-latest) (push) Successful in 25s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 12s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 12s
CI / Catalog signature (push) Successful in 7s
Build & Release / Build (Windows) (push) Successful in 59s
Build & Release / Build (macOS) (push) Successful in 2m1s
Build & Release / Build (Linux) (push) Successful in 1m3s
Build & Release / Signing key smoke test (push) Has been skipped
Build & Release / Publish Release (push) Successful in 18s
|
||
|
|
451e04cd96 |
release: prep 1.4.0 — bump version + finalize changelog
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 12s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 24s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 14s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 13s
CI / Catalog signature (pull_request) Successful in 8s
Bumps __version__ and pyproject to 1.4.0 and rolls CHANGELOG's [Unreleased] section into [1.4.0] - 2026-08-13, opening a fresh [Unreleased]. No behavior change; this is the release-staging PR. After merge, push the v1.4.0 tag to trigger release.yml. |
||
|
|
675490e3c3 |
Merge pull request 'ci: skip test matrix on docs-only changes + add job timeouts' (#108) from ci/harden-runner into main
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 12s
CI / Tests (py3.12 / windows-latest) (push) Successful in 26s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 12s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 11s
CI / Lint (ruff) (push) Successful in 8s
CI / Catalog signature (push) Successful in 7s
|
||
|
|
3768113938 |
Merge pull request 'docs: add CHANGELOG.md (Keep a Changelog) — Unreleased → 1.4.0' (#107) from docs/changelog into main
CI / Tests (py3.12 / windows-latest) (push) Successful in 24s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 12s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 12s
CI / Catalog signature (push) Successful in 7s
CI / Lint (ruff) (push) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 12s
|
||
|
|
583e4e4af0 |
ci: skip test matrix on docs-only changes + add job timeouts
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 13s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 27s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 12s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 13s
CI / Catalog signature (pull_request) Successful in 7s
- paths-ignore '**/*.md' on push/pull_request so a CHANGELOG/README-only PR doesn't spin up the (self-hosted, sometimes-offline) Windows test job. - timeout-minutes on lint (10) / test (15) / catalog-signature (10) so a job that hangs mid-run fails instead of hanging forever. Note: timeout-minutes counts from job start, so it does not rescue a job stuck 'Waiting to run' when the Windows runner is offline — paths-ignore covers the docs case; code PRs still need the runner up. |
||
|
|
38e9cf2b26 |
docs: add CHANGELOG.md (Keep a Changelog); Unreleased → 1.4.0
CI / Lint (ruff) (pull_request) Successful in 13s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 12s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 11s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 11s
CI / Catalog signature (pull_request) Successful in 7s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 1m11s
Starts a per-release changelog. The Unreleased section captures everything merged since v1.3.0 (ssh-mcp v2 support, sidecar detect/edit, version pin, permission pre-flight, hot-reload, light theme, visible update checker, move-to-env, cross-client phase 1) with the ssh-mcp-is-breaking-upstream / BCC-is-additive distinction called out. Past releases backfilled from tags. |
||
|
|
d4ce2647ae |
Merge pull request 'feat(#102): in-app sidecar TOML editor (stacked on #101)' (#104) from feat/102 into main
CI / Lint (ruff) (push) Successful in 7s
CI / Tests (py3.12 / windows-latest) (push) Successful in 21s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 11s
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 6s
|
||
|
|
ece8c99f79 |
Merge pull request 'feat(#101): live hot-reload of external sidecar/config changes' (#103) from feat/101 into main
CI / Catalog signature (push) Successful in 7s
CI / Lint (ruff) (push) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 11s
CI / Tests (py3.12 / windows-latest) (push) Successful in 21s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 12s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 11s
|
||
|
|
66b0101dea |
feat(#102): GUI — in-app sidecar editor (pick-lists + raw fallback)
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 12s
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 11s
CI / Catalog signature (pull_request) Successful in 7s
Reached via a new "Edit config…" button beside the sidecar advisory, shown whenever the selected server has a resolvable sidecar (even before the file exists, so it can be created from BCC). SidecarEditorDialog — a minimal, reviewable first pass: - A section picker (from core.toml_sections), defaulting to [server]. - Pick-lists for the schema enum fields (auth/approvalMode/role) and a range-bounded spinner for port — a layperson can't type auth="sshkey". - A raw-TOML editor showing the full file: the always-available fallback for anything the form doesn't model. - Save applies ONLY the fields the user changed, surgically on top of the raw text (core.update_toml), so comments/unknown keys/other sections round-trip; validates the changed managed values; writes through core.write_sidecar (atomic + backup + chmod 0600). Never touches apply_servers. On success it re-runs the read-only advisories (the same path #101's watcher uses) so "args inert"/permission advisories update live, and confirms the backup + 0600. All decision logic is in bcc_core (unit-tested); this is thin wiring, smoke-tested headlessly (QT_QPA_PLATFORM=offscreen): dialog build, section detection, changed-field diff, surgical save with comment preserved, 0600 applied, and Edit-button visibility (shown for ssh-mcp, hidden for plain stdio + remote). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c5a6bdd1d1 |
feat(#102): core — stdlib TOML read/validate/surgical-write + atomic sidecar writer
The pure, fully-tested half of the in-app sidecar editor. No new runtime dependency: CI's test job installs only `pytest cryptography` (not requirements.txt) and the catalog-signature job imports bcc_core with only cryptography, and the workflow is off-limits — so a top-level TOML import is impossible and any pip TOML dep would leave this code untested/red on CI. - read_toml_section() — lenient, section-aware scalar reader (string/int/ float/bool); values it can't confidently decode are omitted (the writer preserves them regardless). - toml_sections() — ordered profile/section groups for the editor's picker. - set_toml_value() / update_toml() — SURGICAL writer: rewrites only the one key it's asked to, so comments, formatting, ordering and unknown keys/tables round-trip untouched (strictly safer than parse->dict->reserialize, which tomli-w wouldn't comment-preserve either). CRLF-preserving; correct scalar quoting/escaping; creates a missing section; deletes on value=None. - validate_sidecar_values() — enum/port validation against ServerSpec.schema for the MANAGED fields only; unknown keys pass through (preserved, never a save-blocker) — reconciles "reject out-of-enum" with "preserve unknown keys". - write_sidecar() — atomic temp-write + os.replace + rotating backup (reuses the extracted _atomic_write_text + _make_backup) then chmod 0600/0700 via #93's fix_permissions. Never routes through apply_servers (cardinal rule). - _make_backup() generalised to the file's own suffix (JSON naming unchanged), so a .toml sidecar and a .json config keep separate backup pools. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ded1eef2dd |
feat(#101): live hot-reload of external sidecar/config changes
CI / Lint (ruff) (pull_request) Successful in 13s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 25s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 20s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 13s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 12s
CI / Catalog signature (pull_request) Successful in 8s
Detection (#91/#93) previously ran only at profile load / server selection, so a config.toml created or chmod-ed while BCC was running stayed invisible until a restart. Make the view react on its own. Core (pure, unit-tested — CI has no PySide6): - sidecar_watch_paths(): the external paths worth watching for a server — the resolved sidecar file, its directory (so create/delete and atomic-rename replaces register), and the wrong-path/doc file — order- stable and de-duplicated. - sidecar_state_fingerprint(): a hashable snapshot folding the #91 sidecar status and #93 permission status, so the GUI can tell whether the *observable* state actually changed and skip a redundant refresh. - sidecar_state_changed(): explicit, named equality for that decision. GUI (thin wiring, smoke-tested headlessly): - QFileSystemWatcher over the selection's sidecar path(s) + BCC's own loaded config; debounced (300 ms) so a burst of writes doesn't thrash. - Re-arm on every event: an atomic-rename replace drops the inode from the watcher, so wanted paths are re-added before the next check. - Focus-in fallback via changeEvent(ActivationChange) — always works where watchers miss (atomic replaces, not-yet-created files). - recheck_advisories() only recomputes warning labels from the form + filesystem; it never touches field values, so a live reload cannot clobber unsaved edits. - An external edit to BCC's own config surfaces a non-destructive Reload banner (never a silent overwrite); confirm-on-dirty reuses the existing load path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
cdda60da1b |
Merge pull request 'Land ServerSpec cut: spine + sidecar + version + perms (#90–#93)' (#100) from feat/serverspec-cut into main
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 11s
CI / Tests (py3.12 / windows-latest) (push) Successful in 35s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 11s
CI / Catalog signature (push) Successful in 7s
|
||
|
|
0920846c2c |
style: ruff format the union-merge seam
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 12s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 11s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 35s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 11s
CI / Catalog signature (pull_request) Successful in 7s
|
||
|
|
ce82f7b5c3 | Merge remote-tracking branch 'origin/feat/93' into integ | ||
|
|
d2c126a60c |
Merge remote-tracking branch 'origin/feat/92' into integ
# Conflicts: # bcc_core.py # tests/test_core.py |
||
|
|
603d24566d |
test(#93): key injected mode/chmod maps on as_posix() for the Windows runner
CI / Lint (ruff) (pull_request) Successful in 9s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 23s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 12s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 16s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 13s
CI / Catalog signature (pull_request) Successful in 8s
permission_status/fix_permissions wrap paths in the running OS's Path class, so on the windows-latest CI job str(p) uses backslashes and the fixture dict lookups miss — the same portability trap fixed for #91. Normalise with Path(p).as_posix() in the four affected lambdas. Pure test fix; production code unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
0b2827e6b8 |
feat(#93): filesystem permission pre-flight for credential configs (0600/0700)
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 13s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 13s
CI / Lint (ruff) (pull_request) Successful in 8s
CI / Tests (py3.12 / windows-latest) (pull_request) Failing after 24s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 12s
CI / Catalog signature (pull_request) Successful in 8s
ssh-mcp refuses to start if its config is group/world-readable (mode & 0o077 → throws, requiring dir 0700 / file 0600). A GUI user has no idea what chmod 600 means — they just get a dead server. This checks it and offers a one-click fix. Core (pure, POSIX-only, injectable stat/chmod so tests never touch a real file): - permission_status(path): the ssh-mcp rule — any group/other bit set (mode & 0o077) is not-ok; file must be 0600, its dir 0700. Returns None on Windows (modes don't apply) or when the file is absent (nothing to pre-flight). Plain-language problems naming the offending octal mode. - fix_permissions(path): chmod file → 0600, dir → 0700. No-op on Windows; reports an OSError instead of raising. - sidecar_permission_warnings() / sidecar_permission_fix_target(): tie the check to #91's sidecar path resolution so it knows WHICH file to inspect. Mirror the sidecar-warnings shape. GUI: a warning label + "Fix permissions" button in the stdio editor (mirrors the removed-flag surface), shown only when the sidecar exists and is too open. Hidden on Windows and for non-sidecar servers. Smoke-tested headlessly. pytest green (529 passed), ruff + format clean. Closes #93. Part of epic #94. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
2e5d0351b4 |
test(#91): assert sidecar paths via as_posix() for the Windows CI runner
CI / Lint (ruff) (pull_request) Successful in 9s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 22s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 13s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 15s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 13s
CI / Catalog signature (pull_request) Successful in 8s
The CI matrix includes a windows-latest job where str(Path("/Users/…")) renders
with backslashes, so the platform-parameterised sidecar-path assertions failed
there (macOS/Linux passed). Compare .as_posix() instead — separator-normalised
and portable — and note why. Pure test fix; no production-code change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
2cd8e0fb3b |
feat(#92): detect unpinned npx specs, resolve version, one-click pin, drift
CI / Lint (ruff) (pull_request) Successful in 9s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 13s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 21s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 13s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 13s
CI / Catalog signature (pull_request) Successful in 9s
`npx -y ssh-mcp` resolves *latest* on every launch — in one session ssh-mcp went
v1 → v2 and the tool set changed under a running agent, mid-task, with no warning.
This detects that and turns it into a comprehensible pin/upgrade prompt.
Core (pure, no network — reuses parse_version / is_newer_version / the catalog
spec parsers; the resolved-version lookup is fully injectable for tests):
- server_package_spec / server_package_name — the npm spec an npx-style server runs.
- is_unpinned_spec — bare name or dist-tag (@latest/@next) is unpinned; an exact
numeric version is pinned.
- resolved_npx_version — reads the local ~/.npm/_npx cache (highest version wins),
degrades to None cleanly. NO network. find/read injectable.
- pin_spec_transform — rewrite the spec to name@version (mirrors pin_command_path's
(new_data, note) contract). No-op when already pinned / bad version / not npx.
- version_drift_note — "moved X → Y since you pinned" via numeric is_newer_version.
- version_status — the badge's high-level dict (unpinned / pinned_version /
resolved_version / can_pin / drift). A _UNSET sentinel lets callers force an
explicit resolved=None ("unknown") vs. omitting it to do the local lookup.
GUI: a version badge row under the dependency status (mirrors that surface) with a
one-click "Pin to <version>" button, shown only for npx servers; a drift note when
a pinned version has been overtaken. Smoke-tested headlessly.
pytest green (520 passed), ruff + format clean. Closes #92. Part of epic #94.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
8c51c25211 |
feat(#91): sidecar config detection + precedence + verified paths
CI / Tests (py3.12 / windows-latest) (pull_request) Failing after 23s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 46s
CI / Catalog signature (pull_request) Successful in 8s
CI / Lint (ruff) (pull_request) Successful in 36s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 15s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 16s
ssh-mcp v2 reads a TOML sidecar and only falls back to CLI args when that file
is ABSENT — so BCC's managed --host/--user args can be completely inert while
the real config lives in a file BCC never looks at. This adds read-only
truth-telling for that (no sidecar writing).
Core (pure, fully injectable platform/environ/home/exists/read for testing):
- sidecar_path(spec): VERIFIED per-platform TOML location from the package source,
NOT the README (macOS → ~/Library/Application Support/ssh-mcp, Windows → %APPDATA%,
else → ${XDG_CONFIG_HOME:-~/.config}). sidecar_doc_path() is the README path.
- sidecar_status(): resolves exists / has_managed_args / args_inert / wrong_path.
- sidecar_warnings(): mirrors removed_flag_warnings' shape. Reports:
* precedence — "these arguments are inert; the server reads <real path>"
* wrong-path — a TOML at the README path the server never actually reads
* #11 credential scoping — an unprefixed SSH_MCP_PASSWORD shared across 2+
profiles in a multi-profile sidecar (count_toml_profiles is a documented
3.10-safe heuristic; unscoped_credential_warning gates on it).
GUI: a read-only advisory label in the stdio editor (mirrors the removed-flag
label; no fix button — editing the sidecar is a separate deliberate action).
Uses the real platform/env/filesystem so it reflects this machine.
pytest green, ruff + format clean. Closes #91. Part of epic #94.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
e087107710 |
feat(#90): promote FLAG_ENV_MIGRATIONS into a ServerSpec spine
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 11s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 25s
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
Introduce the server-package axis (orthogonal to ClientSpec): a frozen ServerSpec dataclass + SERVER_SPECS registry that the sidecar (#91), version-pin (#92) and permission (#93) work all hang off. - ServerSpec carries env_flags / removed_flags / drift_flags / sidecar_paths / sidecar_doc_path / schema. FLAG_ENV_MIGRATIONS is now a derived view of the registry, so every existing reader and the migration functions keep the exact shape #89 shipped — the migration LOGIC is unchanged, only the DATA grew. - resolve_server_spec(data) is the ServerSpec entry point; detect_migratable_package is a thin name-only wrapper over it (behaviour identical). - Add the verified #4 follow-ups: --sudoPassword AND --suPassword now auto-migrate to SSH_MCP_SUDO_PASSWORD (two flags → one var; existing no-clobber handles it). --disableSudo stays warn-only (sudo is now a role/policy, no env replacement). - Add drift_warnings(): --maxChars=none changed meaning (v1 silently capped at 5000 chars). Warn-only, no auto-fix; matches inline and separate arg forms. - Seed ssh-mcp's verified per-platform sidecar TOML paths and zod enums (auth/approvalMode/role/port) as data for later issues. Pure core + tests, no GUI. One #89 test (sudoPassword now migratable) updated to reflect the intended data growth, with a comment citing the verified facts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
436524bf00 |
Merge pull request 'feat(#88): detect & migrate removed CLI flags into env (ssh-mcp v2)' (#89) from feat/removed-flag-env-migration into main
CI / Tests (py3.12 / windows-latest) (push) Successful in 22s
CI / Lint (ruff) (push) Successful in 9s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 13s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 14s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 14s
CI / Catalog signature (push) Successful in 10s
|
||
|
|
e542ff6e8f |
Merge remote-tracking branch 'origin/main' into feat/removed-flag-env-migration
CI / Lint (ruff) (pull_request) Successful in 11s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 23s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 14s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 17s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 14s
CI / Catalog signature (pull_request) Successful in 9s
# Conflicts: # tests/test_core.py |
||
|
|
dc9e035781 |
Merge pull request '"Move to environment variable" — convert a plaintext secret to ${VAR} (#83)' (#87) from feat/83-move-to-env-var into main
CI / Tests (py3.12 / windows-latest) (push) Successful in 35s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 14s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 15s
CI / Catalog signature (push) Successful in 9s
CI / Lint (ruff) (push) Successful in 9s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 13s
|
||
|
|
7368dcdbff |
feat(#88): detect & migrate removed CLI flags into env (ssh-mcp v2)
CI / Lint (ruff) (pull_request) Successful in 11s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 38s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 12s
CI / Catalog signature (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 12s
ssh-mcp v2 removed --password from the command line and reads
SSH_MCP_PASSWORD instead, so an old config crashes on startup. Add a
data-driven FLAG_ENV_MIGRATIONS registry plus detect_migratable_package,
migrate_removed_flags and removed_flag_warnings in bcc_core, and a
'Fix: move to environment variables' one-click action + warning in the
stdio server editor, with a matching main-window lint line. The literal
value lands in env{} (the only form Claude Desktop honours). 14 tests.
|
||
|
|
694439b6f3 |
feat(#83): variables readout, two clear move actions, args->env relocation
CI / Lint (ruff) (pull_request) Successful in 11s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 30s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 19s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 20s
CI / Catalog signature (pull_request) Successful in 13s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 18s
Field testing showed the feature was doing the right thing but describing it
wrong, and left the moved variable invisible. Reworked per that feedback:
Naming. "Move to environment variable" read like "move into the Environment
variables table"; it actually creates a ${VAR} reference to the shell/OS
environment. Renamed the action to "Replace with a ${VAR} reference (out of
file)…" everywhere, and the dialog now says plainly the secret goes to your
shell environment -- not this file, not the table below.
Visibility (the real gap). A lone ${SECRET_KEY} with nothing saying whether
it's wired up isn't much better than a mystery. New Variables… button opens
ReferencedVarsDialog: every ${VAR} the loaded server references, each with
✓ set / ✓ default / ✗ not set and the exact export/setx line to set it.
Backed by pure core.referenced_env_vars (dedupes across fields, resolves
against a given environment or a default).
Two actions on an args secret, because the user may want either:
* "Replace with a ${VAR} reference (out of file)…" -- secret leaves the
file (needs a client that expands refs; disabled with reason on Desktop).
* "Move into Environment variables (kept in this config)…" -- relocates the
arg into the env block where it's visible and editable. Works on any
client (no ${VAR} needed). core.move_arg_to_env_block drops the flag+value
and sets env[VAR]; the dialog warns it changes how the server launches.
Both args actions run through ServerEditor (moving into env touches args AND
the env table), which reloads the form from the transformed data.
Tests: +10 core (referenced_env_vars dedupe/status/default; move_arg_to_env_block
flag+value removal, bare positional, None on bad target). 488 passed, ruff
clean. New dialogs/menus are GUI, untestable in CI as before.
Refs #83
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKwBecy6N83jnqQmw8ezwE
|
||
|
|
4743c4a995 |
feat(#83): offer move-to-env on args rows; explain the gate instead of an empty menu
CI / Lint (ruff) (pull_request) Successful in 19s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 24s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 29s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 29s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 31s
CI / Catalog signature (pull_request) Successful in 22s
Two things surfaced testing the GUI:
1. Args rows showed the secret warning but no move action -- the args
editor is a free-text widget, not a table, and was deliberately left out
of the first cut. Wired it up: ArgsEdit gains a context menu that offers
"Move to environment variable…" on exactly the args that look like a
credential. New pure core: secret_arg_indices (which args are secrets,
mirroring args_secret_warning per-index) and suggested_env_var_for_arg
(default var name from the preceding flag -- `--api-key <secret>` ->
API_KEY, else SECRET). The move replaces that one arg line with ${VAR}
and copies the secret to the clipboard, same contract as the tables.
2. On a Claude Desktop profile (or any non-expanding client) the menu showed
NOTHING, so it read as broken. Now a real stored secret always shows the
item -- enabled on a client that expands references, or disabled with the
reason ("unavailable for Claude Desktop -- it doesn't expand ${VAR}") so
the gate is visible rather than silent. Applies to env, headers and args.
Not a change: after converting, env_ref_warnings still notes a variable that
isn't set in the environment. That's #82's advisory doing its job -- the user
runs the export line the dialog handed them; auto-adding a ':-default' would
bake a fallback back into the config and defeat moving the secret out.
Tests: +5 core (secret_arg_indices for token/flag-value/embedded-URL/
reference-excluded, suggested_env_var_for_arg with and without a flag).
483 passed, ruff clean. GUI wiring (context menus) remains untestable in CI.
Refs #83
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKwBecy6N83jnqQmw8ezwE
|
||
|
|
8fdbe90b37 |
feat: "Move to environment variable" — convert a plaintext secret to ${VAR} (#83)
CI / Lint (ruff) (pull_request) Successful in 14s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 21s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 20s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 21s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 23s
CI / Catalog signature (pull_request) Successful in 17s
Follow-up to #76/#82: BCC warns when a config holds a raw credential and
points at ${VAR}, but gave no way to make the change. This adds the
one-click conversion, right-click a secret row in the env or headers table.
The value is about to leave the file, so the action's real job is handing
the secret back before it does:
- Core (pure, tested): sanitize_env_var_name (key -> legal upper-case shell
name; 'api-key' -> API_KEY, '2fa' -> _2FA, non-ASCII/empty handled),
shell_export_lines (the exact export/setx line, POSIX single-quoted
safely), move_value_to_env_ref (data in -> new data out, replaces one
env/header/args value with ${VAR}, returns the removed secret; never
mutates the input; None if the target is missing, non-string, or already a
reference), can_move_value_to_env_ref (offer only a real stored secret, not
already a ref, AND only on a client that expands references -- offering it
on Claude Desktop would author a config that reaches the server as literal
${VAR}, the exact failure #76 exists to prevent), and is_env_var_set (skip
the ceremony when the variable already looks set).
- GUI: KeyValueTable gains a context menu gated on can_move_value_to_env_ref
(so it never appears on a non-secret row or a Claude Desktop profile).
MoveToEnvDialog lets the user name the variable (defaulting to the
sanitised key), shows the platform-appropriate shell line live, notes when
the variable already looks set, and on accept copies the secret to the
clipboard before the cell is replaced with the reference. Wired through
ServerEditor.set_profile_provider so the tables know which client is loaded.
Scope note: env and headers rows for now. The core already handles args by
index; wiring the args editor (a free-text widget, not a table) is a small
follow-up, deliberately not bundled here.
Tests: +15 core (name sanitisation incl. non-ASCII/leading-digit/empty,
POSIX quote safety, the gate across secret/non-secret/already-ref/
non-expanding-client, env+headers+args rewrite, input-not-mutated,
missing/non-string/already-ref -> None, is_env_var_set). 478 passed, ruff
clean. GUI is untestable in CI (no PySide6); the decision logic all lives in
bcc_core and is tested there.
Closes #83
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKwBecy6N83jnqQmw8ezwE
|
||
|
|
072a5cdc08 |
Merge PR #86: disambiguate project profiles with the same basename (#74)
CI / Lint (ruff) (push) Successful in 12s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 18s
CI / Tests (py3.12 / windows-latest) (push) Successful in 35s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 18s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 18s
CI / Catalog signature (push) Successful in 14s
Two repos both named "app" no longer both render as "Project: app": disambiguate_project_labels widens colliding labels toward the root (Project: work/app vs personal/app), discover_project_configs now requires a .mcp.json that parses to a dict (skipping arrays/garbage that only failed on open), and the full path goes in the combo tooltip. Pure core + 6 tests; the only GUI change is one setItemData line. 463 passed, ruff clean, CI green incl. Windows. Closes #74 |
||
|
|
0191a93eb9 |
fix: disambiguate project profiles that share a directory basename (#74)
CI / Lint (ruff) (pull_request) Successful in 12s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 22s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 19s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 19s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 19s
CI / Catalog signature (pull_request) Successful in 11s
discover_project_configs labelled every project by basename alone, so
~/work/app/.mcp.json and ~/personal/app/.mcp.json both read as
"Project: app". Paths dedupe correctly, so both profiles existed -- they
were just indistinguishable in the picker, and picking the wrong one meant
editing, backing up, and writing the wrong repo's config. api/web/app/
server/client as repo names make this common.
- disambiguate_project_labels(dirs): pure, testable. Labels stay
"Project: <name>" until a basename collides, then only the colliding
ones widen toward the root one component at a time
("Project: work/app" vs "Project: personal/app"), widening further if the
parent also collides. The common no-collision case is unchanged.
- Full path goes in the combo item's ToolTipRole, so a profile is always
verifiable by hover regardless of label.
- Tightened the "is this a project config?" check: the docstring claimed a
top-level object but the code only checked is_file(), so a .mcp.json that
was a JSON array or garbage still became a profile and only failed on
open. It now must parse (strict) to a dict, else it's skipped.
Tests: +6 (no-collision basename, colliding widen, deeper widen, and
discover_project_configs disambiguation + skipping array/garbage/missing).
463 passed, ruff clean.
Closes #74
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKwBecy6N83jnqQmw8ezwE
|
||
|
|
5add9b0ce0 |
Merge PR #85: ClientSpec adapter refactor — cross-client phase 1 (#5)
CI / Lint (ruff) (push) Successful in 21s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 24s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 25s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 30s
CI / Tests (py3.12 / windows-latest) (push) Successful in 25s
CI / Catalog signature (push) Successful in 24s
Phase 1 of cross-client support: introduce the ClientSpec adapter and route Claude Desktop + Claude Code through it with no behavior change. Parameterizes servers_key/disabled_key/discovery and the entry translation seam; extract_servers/apply_servers/_server_sections/external_change_summary default to Claude's layout so no-spec calls are byte-identical. Verified: 458 tests + a synthetic non-mcpServers client proving the seam generalizes, CI green on all platforms incl. Windows, and the live GUI confirmed loading/saving identically. requirements.txt now declares cryptography as the runtime dep it always was. Refs #5 |
||
|
|
57fd3cb6e3 |
fix: declare cryptography as a runtime dependency
CI / Lint (ruff) (pull_request) Successful in 24s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 38s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 28s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 31s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 28s
CI / Catalog signature (pull_request) Successful in 19s
bcc_core imports cryptography at module load (catalog signature verification), so it's required to even start the app -- but requirements.txt listed only PySide6, so a from-source run died with ModuleNotFoundError: No module named 'cryptography'. The frozen release builds were unaffected because PyInstaller follows the import, which is why this never surfaced until someone ran the GUI from source to review this branch. Move cryptography into requirements.txt (runtime) and re-label it in requirements-dev.txt as runtime rather than test-only; the version pin is unchanged (>=42.0). Refs #5 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EKwBecy6N83jnqQmw8ezwE |
||
|
|
4a95b370b9 |
refactor: introduce ClientSpec adapter; route Claude Desktop + Code through it (#5)
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 24s
CI / Lint (ruff) (pull_request) Successful in 21s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 32s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 31s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 35s
CI / Catalog signature (pull_request) Successful in 26s
Cross-client support (Cursor / Windsurf / VS Code) was blocked on Claude's layout being hard-coded throughout the code: servers always under the literal "mcpServers" key, a fixed set of Claude file locations, and "which client is this?" answered by sniffing a filename. Adding a client that differs on any of those axes meant chasing those assumptions through a dozen sites. This is phase 1 of #5: the keystone refactor, with NO behaviour change. It adds a ClientSpec adapter that captures the three things that vary across clients -- the top-level servers key, the config discovery paths, and the per-server value shape -- plus the capability flags that were previously computed inline from a filename (does the client expand ${VAR}? can we offer Restart?). - ClientSpec (frozen dataclass): servers_key, disabled_key, config_filename, expands_env_refs, supports_restart, and entry_to_internal/entry_from_internal -- the per-server translation seam, identity for any mcpServers-shaped client, the single point a differently-shaped client (VS Code's type/inputs form) overrides. - CLAUDE_DESKTOP and CLAUDE_CODE specs; both use mcpServers + the existing parking key, so their translation is the identity and nothing changes for today's users. resolve_client(path) reproduces the old filename rule exactly; each Profile now carries its resolved .client. - extract_servers / apply_servers / _server_sections / external_change_summary take an optional spec and default to Claude's layout, so every existing call site and test that omits a spec is byte-for-byte unchanged. The cardinal rule now generalises: apply_servers only ever writes the client's own two keys, parameterised rather than hard-coded. - profile_targets_claude_desktop and client_expands_env_refs are now thin reads off the profile's spec -- one source of truth for client identity instead of scattered filename checks -- with identical answers. - GUI: the load, Copy-to, save and stale-merge paths pass the profile's spec into the core calls. Mechanical; no logic moved into bcc.py (which CI can't test -- no PySide6). Tests: +14. Existing suite unchanged and green (behaviour preservation). A synthetic non-mcpServers spec ("servers" key, a different disabled key, a per-server `type` field) exercises the whole pipeline -- extract, apply, masking, external-change diff -- proving the seam actually generalises before any real client depends on it. 458 passed, 1 skipped; ruff clean. Refs #5 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EKwBecy6N83jnqQmw8ezwE |
||
|
|
f168079755 |
Merge PR #82: author ${VAR} references, gated on client expansion (#76)
CI / Tests (py3.12 / windows-latest) (push) Successful in 39s
CI / Lint (ruff) (push) Successful in 5m0s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 39s
CI / Tests (py3.12 / ubuntu-latest) (push) Successful in 35s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 34s
CI / Catalog signature (push) Successful in 25s
Adds the authoring + safety layer for ${VAR} references: expand_env_refs preview, per-client gating via profile_targets_claude_desktop (Claude Code expands natively, Desktop does not), and fixes two backwards behaviours (masking hid placeholders; args_secret_warning fired on the recommended fix). BCC never expands on write. 444 tests, ruff clean.
Closes #76
|
||
|
|
a73f2e3883 |
feat: author ${VAR} references, gated on whether the client expands them (#76)
CI / Lint (ruff) (pull_request) Successful in 9s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 11s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 10s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 12s
CI / Catalog signature (pull_request) Successful in 7s
CI / Tests (py3.12 / windows-latest) (pull_request) Has been cancelled
The blocker on this issue was whether BCC or the client does the expanding.
Answer, from Anthropic's docs: Claude Code expands ${VAR} and
${VAR:-default} itself, in command, args, env, url and headers, for both
project .mcp.json and user-scope ~/.claude.json. Claude Desktop has no
documented support.
So this is a per-client capability, not a global one, and BCC does NOT
expand on write: resolving a reference into the file would put the secret
back on disk -- the whole thing the user is avoiding -- and would defeat a
feature the client already implements correctly. BCC authors, validates and
warns; expand_env_refs exists to preview what the client will do.
Semantics mirror the documented ones exactly, including the unusual bit:
an unset variable with no default is left as literal ${VAR} text rather
than blanked, because that is what Claude Code passes through.
Gating uses the existing profile_targets_claude_desktop(), so a config that
is correct under Claude Code and broken under Desktop is reported against
whichever profile is actually loaded. The two warnings are worded
differently on purpose -- 'this client will never expand these' is a
different problem from 'this variable looks unset here'.
Two existing behaviours were backwards for this feature and are fixed:
- Secret masking hid placeholders. is_secret_key('API_KEY') is true, so
${API_KEY} rendered as dots -- making a reference indistinguishable from
a stored credential, which is the one distinction that makes the feature
worth adopting. should_mask_value() now skips references, in the table
delegate, _redact_server_data and redact_args alike.
- args_secret_warning fired on placeholders. Moving a token into ${VAR} is
the recommended fix for that warning; continuing to warn punished the
fix. It now skips references while still flagging a real secret that
follows one.
Real secrets are still masked everywhere they were before -- asserted, not
assumed.
Refs #76
|
||
|
|
7ff4f6e5c0 |
Merge PR #81: make the update checker visible — persistent banner + Help menu item (#78, #79)
CI / Lint (ruff) (push) Successful in 6s
CI / Tests (py3.10 / ubuntu-latest) (push) Successful in 10s
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
CI / Tests (py3.12 / windows-latest) (push) Has been cancelled
|
||
|
|
7517e16b15 |
Merge branch 'main' into fix/78-79-update-visibility
CI / Lint (ruff) (pull_request) Successful in 6s
CI / Catalog signature (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 / Tests (py3.12 / windows-latest) (pull_request) Has been cancelled
Three conflicts, two of them semantic rather than textual:
- bcc.py QSS: this branch added the noticeBanner rules using the old
module-level constants ({MUTED}, {ACCENT}); main had since moved the
stylesheet onto palette slots ({p.muted}). Took main's form and
translated the notice rules into it -- picking either side wholesale
would have either dropped the banner styling or reintroduced globals
that test_stylesheet_builder_has_no_hardcoded_colours now forbids.
- bcc.py methods: both sides appended to MainWindow (update-notice
handlers vs theme handlers). Additive, kept both.
- tests/test_core.py: the usual EOF append. Kept both blocks.
_build_menu_bar auto-merged cleanly (View menu above, Help menu below);
verified both are present with their menu roles intact.
Verified: 272 test functions = 265 (main) + 7 (this branch), no
duplicates; 421 passed, ruff clean.
|
||
|
|
9a0433225e |
Merge PR #80: light theme + system-following, dark preserved exactly (#75)
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 11s
CI / Tests (py3.13 / ubuntu-latest) (push) Successful in 10s
CI / Catalog signature (push) Successful in 7s
CI / Tests (py3.12 / windows-latest) (push) Has been cancelled
|
||
|
|
fa82d30087 |
Merge branch 'main' into feat/75-theming
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 10s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 11s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 10s
CI / Catalog signature (pull_request) Successful in 7s
CI / Tests (py3.12 / windows-latest) (pull_request) Has been cancelled
Union conflict at the end of tests/test_core.py -- both branches appended a test block. Kept both, main's #72/#73 block first. Verified: 265 test functions = 238 baseline + 15 (#77) + 12 (theming), no duplicates. |
||
|
|
05b00a40c0 |
Merge PR #77: tolerate non-object server values; keep named sets across a merge (#72, #73)
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.13 / ubuntu-latest) (push) Successful in 11s
CI / Catalog signature (push) Successful in 7s
CI / Tests (py3.12 / windows-latest) (push) Has been cancelled
|
||
|
|
3068e74e5c |
fix: make the update checker visible -- persistent banner + a menu item
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.13 / ubuntu-latest) (pull_request) Successful in 10s
CI / Catalog signature (pull_request) Successful in 6s
CI / Tests (py3.12 / windows-latest) (pull_request) Has been cancelled
Reported from the field: running v1.2 against a repo with v1.3.0 published gave no prompt, and there appeared to be no way to check manually. The checker itself works; it was invisible, for two reasons. #78 -- the notice was written to the shared status label, which 21 other call sites rewrite. The check runs off-thread and lands a second or two after launch, right as the user starts clicking, so the next selection or refresh wiped it. Exactly the bug fixed for the MSIX warning in #35, which got a persistent banner; that fix was never carried to the update notice. Adds NoticeBanner: a persistent, dismissible notice carrying its own action button. It's a shared widget rather than a second bespoke banner, so the next thing needing the user's attention doesn't reach for the status bar again. (The MSIX banner still uses its own QLabel -- migrating it is a follow-up, deliberately not bundled with a bug fix.) #79 -- the only 'Check for updates' affordance was a button inside the About dialog, which is not where anyone looks. Worse, the About action was created without a menu role, and Qt auto-assigns AboutRole to actions whose text begins with 'About', relocating it into the macOS application menu -- so the notice's own hint, 'Help > About to view it', pointed at a menu that on macOS doesn't contain the item. Help now has its own 'Check for updates...' item with an explicit ApplicationSpecificRole, and the About action states its AboutRole rather than inheriting it invisibly. The menu-driven check is never throttled and always reports back -- the user asked, so silence would read as broken. The decision and the wording live in core.update_notice() because the test suite has no PySide6 (CI installs pytest + cryptography only), so anything in bcc.py is untestable. A test asserts the notice text names no menu path, which is what went stale here in the first place. Closes #78 Closes #79 |
||
|
|
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 |
||
|
|
da20eb2fdb |
fix: tolerate non-object server values on load; keep named sets across a merge
CI / Lint (ruff) (pull_request) Successful in 13s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 12s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 11s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 12s
CI / Catalog signature (pull_request) Successful in 8s
CI / Tests (py3.12 / windows-latest) (pull_request) Has been cancelled
Two silent-failure bugs found auditing the v1.3.0 features. #72 -- extract_servers called dict() on every server value, so a config that was valid JSON but held a non-object server ("foo": "oops", a number, a list, null) raised on load. The strict parse succeeded, so the repair path never saw it, and the call sat outside the load try/except: an unhandled traceback with the window half-swapped to the new profile. It also made the #54 schema lint unreachable for the most likely hand-edit mistake -- the load died before the linter ran. Malformed values are now preserved verbatim on ServerEntry.raw (behind a NO_RAW sentinel, since a literal JSON null is itself a malformed entry worth keeping) and written back untouched on Save, so nothing is silently deleted. lint_servers names the offending entry instead. #73 -- the stale-file "Merge & save" path reloaded the file from disk and re-applied the user's servers, but apply_servers only writes mcpServers and _disabledMcpServers. Named server sets live under _bccServerSets in the same file, so a set saved that session was dropped from disk and then from memory, with no warning, on the path the user picks because it sounds like the safe one. BCC-owned keys are now declared in BCC_OWNED_KEYS and carried across by carry_owned_keys, which reports genuinely contested keys so the status line can say so. Deliberately one-directional: a key absent locally is left alone on disk, because 'user deleted their last set' and 'another machine just added sets' are indistinguishable and deleting someone else's data is the worse failure. Closes #72 Closes #73 |
||
|
|
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
|
||
|
|
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
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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
|
||
|
|
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. |
||
|
|
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
|