Epic: model the server-package axis — ssh-mcp field notes → roadmap #94

Open
opened 2026-08-12 02:34:47 -04:00 by the_og · 2 comments
Owner

Tracking issue for the 18 field notes from the 2026-08-02 ssh-mcp setup (BCC-ROADMAP-ssh-mcp-notes-COWORK.md). Through-line: BCC models which host reads a config (ClientSpec) but not which server package it's editing. The seed we shipped (FLAG_ENV_MIGRATIONS, #89) grew into a ServerSpec axis; the rest falls out additively.

Non-reversing rule: sidecar (TOML) editing is a new write target — its own writer reuses write_config's atomic-rename + _make_backup; it does not go through apply_servers. ClientSpec untouched; ServerSpec orthogonal.

Already shipped before this epic — no action

  • #5 backups/atomic/rollback · #12 redaction · #14 Health column · #15 stderr · #18 multi-client phase 1 · #4 core migration (#88/#89)

First cut — ✅ MERGED (#100)

  • #90 ServerSpec spine · [x] #91 sidecar detection/precedence · [x] #92 version pin/drift · [x] #93 permission pre-flight

Second cut — ✅ MERGED (#103, #104)

  • #101 — hot-reload / live re-detect (QFileSystemWatcher + focus-in fallback + debounce; non-destructive reload banner)
  • #102 — in-app sidecar editor (pick-lists + raw fallback; surgical stdlib-only TOML writer, no new dep; own atomic+backup writer, chmod 0600)

Follow-ups from the #102 click-through

  • #105 — sidecar editor: section-switch drops unsaved pick-list edits (P2)
  • #106 — sidecar is machine-global: warn before edit + scope advisory/editor to the server's profile (P1 for the warning)

Deferred (heavier / GUI-central — supervised)

  • #6 pre-flight handshake + tool list before restart
  • #16 guided setup form (args-or-TOML per version)
  • #10 auth = agent/keychain for server creds
  • #17 import from ~/.ssh/config
  • #9 SSH/stdio "Test connection"
  • #12 presentation mode
  • #13 interrupt-aware restart polish

Catalog-signing threat model settled separately — decision notes on #61 / #71.

Tracking issue for the 18 field notes from the 2026-08-02 ssh-mcp setup (`BCC-ROADMAP-ssh-mcp-notes-COWORK.md`). Through-line: BCC models *which host reads a config* (`ClientSpec`) but not *which server package it's editing*. The seed we shipped (`FLAG_ENV_MIGRATIONS`, #89) grew into a `ServerSpec` axis; the rest falls out additively. **Non-reversing rule:** sidecar (TOML) editing is a *new* write target — its own writer reuses `write_config`'s atomic-rename + `_make_backup`; it does **not** go through `apply_servers`. `ClientSpec` untouched; `ServerSpec` orthogonal. ### Already shipped before this epic — no action - #5 backups/atomic/rollback · #12 redaction · #14 Health column · #15 stderr · #18 multi-client phase 1 · #4 core migration (#88/#89) ### First cut — ✅ MERGED (#100) - [x] #90 ServerSpec spine · [x] #91 sidecar detection/precedence · [x] #92 version pin/drift · [x] #93 permission pre-flight ### Second cut — ✅ MERGED (#103, #104) - [x] #101 — hot-reload / live re-detect (QFileSystemWatcher + focus-in fallback + debounce; non-destructive reload banner) - [x] #102 — in-app sidecar editor (pick-lists + raw fallback; **surgical stdlib-only TOML writer, no new dep**; own atomic+backup writer, chmod 0600) ### Follow-ups from the #102 click-through - [ ] #105 — sidecar editor: section-switch drops unsaved pick-list edits (P2) - [ ] #106 — sidecar is machine-global: warn before edit + scope advisory/editor to the server's profile (P1 for the warning) ### Deferred (heavier / GUI-central — supervised) - [ ] #6 pre-flight handshake + tool list before restart - [ ] #16 guided setup form (args-or-TOML per version) - [ ] #10 auth = agent/keychain for server creds - [ ] #17 import from `~/.ssh/config` - [ ] #9 SSH/stdio "Test connection" - [ ] #12 presentation mode - [ ] #13 interrupt-aware restart polish Catalog-signing threat model settled separately — decision notes on #61 / #71.
the_og added the P1 label 2026-08-12 02:34:47 -04:00
Author
Owner

Overnight cut — #90–#93 all implemented, PR'd, CI-green ✅

Worked the four filed issues unattended, one PR each off fresh origin/main. Nothing merged, tagged, or released. Every PR stays open for review.

PR Branch Issue Base CI
#96 feat/90 #90 ServerSpec spine (P1) main ✅ green
#97 feat/91 #91 Sidecar detection + precedence (P0) feat/90 ✅ green
#98 feat/92 #92 Version pin/drift (P0) feat/90 ✅ green
#99 feat/93 #93 Permission pre-flight (P1) feat/91 ✅ green

Stacked — #91/#92 are siblings off #90; #93 hangs off #91 (needs its sidecar path resolution). Each PR's base is its parent branch so per-PR diffs are clean. Merge order: #90 → #91 → #92 → #93 (merge #90 first; the rest retarget to main cleanly). Prefer merge-commit/rebase, not squash (squashing a parent breaks a child's base).

Suite: 509 → 529 passing (1 skipped). ruff check + format --check clean on all four. Touched only bcc_core.py / bcc.py / tests/test_core.py. No catalog/signing/CI-workflow edits. No network calls; no sidecar-content writer added (cardinal rule intact — apply_servers still writes only mcpServers/_disabledMcpServers).

Decisions / caveats to review

  • #90 intentionally updated one #89 test. test_removed_flag_warnings_covers_migratable_and_manual encoded the old warn-only --sudoPassword. Verified facts + issue #90 mandate adding sudo/su → SSH_MCP_SUDO_PASSWORD to the auto-migrate map, so it now moves into env. The migration logic is byte-for-byte unchanged — only the registry data grew. All other #89 tests untouched.
  • #91 count_toml_profiles is a documented heuristic (no tomllib on py3.10; exact ssh-mcp profile schema unverifiable here). It only advises, never edits/blocks — worth confirming against the real package schema.
  • Cross-platform tests: the CI Windows runner renders str(Path("/Users/…")) with backslashes; path assertions use .as_posix(). #91 and #93 each needed one follow-up commit for this — caught before declaring green.

GUI needs a human click-through (headless smoke confirms wiring, not live firing — no ssh-mcp config.toml on the CI box)

  1. #91 sidecar advisory (args-inert + wrong-path) with a real config.toml present.
  2. #92 version badge + "Pin to <v>" button on an npx -y ssh-mcp server (resolve → pin → badge flips to pinned; drift note when the cache moved past a pin).
  3. #93 permission warning + "Fix permissions" button against a config.toml at 0644 (POSIX only).
  4. New rows hide for remote servers / no selection.

#90 has no GUI (pure core). Full write-up in OVERNIGHT-REPORT-COWORK.md at the repo root.

## Overnight cut — #90–#93 all implemented, PR'd, CI-green ✅ Worked the four filed issues unattended, one PR each off fresh `origin/main`. **Nothing merged, tagged, or released.** Every PR stays open for review. | PR | Branch | Issue | Base | CI | |----|--------|-------|------|----| | #96 | `feat/90` | #90 ServerSpec spine (P1) | `main` | ✅ green | | #97 | `feat/91` | #91 Sidecar detection + precedence (P0) | `feat/90` | ✅ green | | #98 | `feat/92` | #92 Version pin/drift (P0) | `feat/90` | ✅ green | | #99 | `feat/93` | #93 Permission pre-flight (P1) | `feat/91` | ✅ green | **Stacked** — #91/#92 are siblings off #90; #93 hangs off #91 (needs its sidecar path resolution). Each PR's base is its parent branch so per-PR diffs are clean. **Merge order: #90 → #91 → #92 → #93** (merge #90 first; the rest retarget to `main` cleanly). Prefer merge-commit/rebase, **not squash** (squashing a parent breaks a child's base). Suite: **509 → 529** passing (1 skipped). `ruff check` + `format --check` clean on all four. Touched only `bcc_core.py` / `bcc.py` / `tests/test_core.py`. No catalog/signing/CI-workflow edits. No network calls; no sidecar-content writer added (cardinal rule intact — `apply_servers` still writes only `mcpServers`/`_disabledMcpServers`). ### Decisions / caveats to review - **#90 intentionally updated one #89 test.** `test_removed_flag_warnings_covers_migratable_and_manual` encoded the old warn-only `--sudoPassword`. Verified facts + issue #90 mandate adding sudo/su → `SSH_MCP_SUDO_PASSWORD` to the auto-migrate map, so it now moves into env. **The migration logic is byte-for-byte unchanged — only the registry data grew.** All other #89 tests untouched. - **#91 `count_toml_profiles` is a documented heuristic** (no `tomllib` on py3.10; exact ssh-mcp profile schema unverifiable here). It only advises, never edits/blocks — worth confirming against the real package schema. - **Cross-platform tests:** the CI Windows runner renders `str(Path("/Users/…"))` with backslashes; path assertions use `.as_posix()`. #91 and #93 each needed one follow-up commit for this — caught before declaring green. ### GUI needs a human click-through (headless smoke confirms wiring, not live firing — no ssh-mcp `config.toml` on the CI box) 1. **#91** sidecar advisory (args-inert + wrong-path) with a real `config.toml` present. 2. **#92** version badge + "Pin to `<v>`" button on an `npx -y ssh-mcp` server (resolve → pin → badge flips to pinned; drift note when the cache moved past a pin). 3. **#93** permission warning + "Fix permissions" button against a `config.toml` at 0644 (POSIX only). 4. New rows hide for remote servers / no selection. #90 has no GUI (pure core). Full write-up in `OVERNIGHT-REPORT-COWORK.md` at the repo root.
Author
Owner

Overnight update — #101 and #102 landed (stacked, both CI-green, not merged).

Overnight report 2 — hot-reload (#101) + in-app sidecar editor (#102)

Two stacked PRs, both CI-green, non-destructive. Nothing merged.

PRs (recommended review/merge order: #101 → #102)

PR Issue Branch Base CI
#103 #101 — live hot-reload feat/101 main ✅ green
#104 #102 — in-app sidecar editor feat/102 feat/101 ✅ green

Branches are stacked (not siblings), per the mandate: feat/102 is branched off feat/101 and its PR base is set to feat/101, so #104's diff is only the editor. Merge #101 first; then #104's base auto-retargets to main (or rebase). This avoids last round's silent sibling-merge conflict.

Dependency decision — no new runtime dependency (this is the load-bearing call)

The task floated tomli+tomli-w or a hand-roll using tomllib/tomli for reads. Neither survives the real CI constraint, which I discovered by reading .github/workflows/ci.yml (off-limits to edit):

  • The test job runs pip install pytest cryptography — not requirements.txt.
  • The catalog-signature job does import bcc_core with only cryptography installed.

Therefore: (a) bcc_core must import with only cryptography present → no top-level TOML import is possible; and (b) any pip TOML dep would leave the new core untested or red on CI, since CI never installs it and I can't change the workflow. Even the task's option (b) fails: the tomllib read path is untested on the 3.10 runner (no stdlib tomllib).

Chosen: stdlib-only, uniform across 3.10/3.12/3.13 — a lenient scalar reader + a surgical line-editing writer that rewrites only the one key it's asked to. Comments, formatting, ordering and unknown keys/tables round-trip untouched — strictly safer than parse→dict→reserialize, and tomli-w wouldn't comment-preserve anyway. Runtime deps stay exactly PySide6 + cryptography. requirements.txt unchanged.

What shipped

#101 — hot-reload (PR #103)

  • Core (unit-tested): sidecar_watch_paths (file + its dir + doc path, de-duped), sidecar_state_fingerprint (hashable snapshot folding #91 sidecar status + #93 permission status), sidecar_state_changed.
  • GUI (thin wiring): QFileSystemWatcher over the selection's sidecar + BCC's own config, debounced 300 ms; re-arm on every event (atomic-rename replaces drop the inode); focus-in fallback via changeEvent(ActivationChange); recheck_advisories() recomputes only warning labels (never field values, so it can't clobber unsaved edits); an external edit to BCC's own config shows a non-destructive Reload banner (confirm-on-dirty), never a silent overwrite.

#102 — in-app sidecar editor (PR #104)

  • Core (unit-tested): read_toml_section, toml_sections, set_toml_value/update_toml (surgical, CRLF-safe, escaping, create-section, delete-on-None), validate_sidecar_values (enum/port on managed fields only; unknown keys pass through), write_sidecar (atomic + rotating backup + chmod 0600/0700 via #93; never apply_servers). Refactor: _make_backup/write_config now share _atomic_write_text; _make_backup keys on the file suffix (JSON naming byte-identical, so a .toml sidecar and .json config keep separate backup pools).
  • GUI (thin wiring): "Edit config…" button beside the sidecar advisory → SidecarEditorDialog (section picker, enum pick-lists, range-bounded port spinner, raw-TOML fallback). Save applies only changed managed fields on top of the raw text, validates, writes, then re-runs advisories through #101's path so they update live.

GUI elements needing a human click-through

CI has no PySide6, so none of the below is exercised by CI — I smoke-tested each headlessly (QT_QPA_PLATFORM=offscreen: dialog build, section detection, changed-field diff, surgical save with comment preserved + 0600 applied, and Edit-button visibility). Please verify interactively:

#101 — hot-reload

  1. Add an ssh-mcp server (npx -y ssh-mcp --host=h --user=u), select it.
  2. In a terminal create the sidecar (macOS): mkdir -p ~/Library/Application\ Support/ssh-mcp && printf '[server]\nhost="h"\n' > ~/Library/Application\ Support/ssh-mcp/config.toml. Within ~1 s the "arguments are inert" advisory appears — no restart.
  3. chmod 644 that file → the permission advisory appears; chmod 600 → it clears.
  4. Edit BCC's own config JSON in another editor and save → the Reload banner appears; click Reload from disk (prompts if you have unsaved edits — confirm it does not clobber them).
  5. Delete the config.toml → advisory clears. Alt-tab away and back to confirm the focus-in fallback also refreshes.

#102 — sidecar editor

  1. With the ssh-mcp server selected, an "Edit config…" button shows beside the advisory (also shows before the file exists, so you can create it).
  2. Click it. With no file yet, the raw editor is empty and the section combo shows (top level). Pick a [server] section (type [server] in raw or use the combo once a section exists), set role = admin via the pick-list, Save.
  3. Confirm: the dialog reports the backup name + 0600; ls -l shows -rw-------; the "args inert" advisory now shows (that's #101 firing).
  4. Re-open, change port (spinner) and auth (pick-list), add a # comment in the raw text, Save. Re-open → the comment survived and the values changed (surgical round-trip).
  5. Confirm invalid enum values are structurally impossible via the pick-lists.

Known limitations (by design; none block — noted for click-through)

  • Section-switch rebaselines the form. Changing the section combo re-reads the pick-lists from raw text for the new section and resets the "changed since load" baseline. So pick-list edits made in section A, then abandoned by switching to section B before Save, are silently dropped. Fine for a first pass; the raw editor is always the source of truth.
  • Raw-text edits bypass schema validation, intentionally. Save validates only the form-changed managed fields, not raw-editor content — typing auth = "sshkey" directly in raw and saving is not rejected. This is the deliberate reconciliation of "reject out-of-enum" vs "preserve unknown keys": whole-file validation would block saves on a pre-existing invalid/unknown key the user never touched. The pick-list guarantee is form-only; the raw editor is the power-user escape hatch.
  • First-pass editor scope. One section targeted at a time; multi-profile editing is via the section picker + raw text. No comment-authoring UI (comments are preserved, not created). These are additive follow-ups if wanted.

Nothing skipped / blocked

Both issues landed CI-green. No irreversible actions taken: no merges, no force-push, no tags, no pushes to main, no touches outside bcc_core.py / bcc.py / tests/ (requirements.txt untouched — zero new deps). apply_servers unchanged; ClientSpec/ServerSpec unchanged (added on top).

Cardinal-rule check

apply_servers still only writes mcpServers/_disabledMcpServers. The sidecar editor writes a different file through its own write_sidecar, which reuses write_config's atomic-rename + _make_backup but never routes through apply_servers (there's an explicit test asserting no server keys leak into a sidecar write).

**Overnight update — #101 and #102 landed (stacked, both CI-green, not merged).** # Overnight report 2 — hot-reload (#101) + in-app sidecar editor (#102) Two stacked PRs, both CI-green, non-destructive. **Nothing merged.** ## PRs (recommended review/merge order: #101 → #102) | PR | Issue | Branch | Base | CI | |----|-------|--------|------|----| | [#103](https://git.avezzano.io/the_og/better-claude-config/pulls/103) | #101 — live hot-reload | `feat/101` | `main` | ✅ green | | [#104](https://git.avezzano.io/the_og/better-claude-config/pulls/104) | #102 — in-app sidecar editor | `feat/102` | **`feat/101`** | ✅ green | Branches are **stacked** (not siblings), per the mandate: `feat/102` is branched off `feat/101` and its PR base is set to `feat/101`, so #104's diff is only the editor. Merge #101 first; then #104's base auto-retargets to `main` (or rebase). This avoids last round's silent sibling-merge conflict. ## Dependency decision — **no new runtime dependency** (this is the load-bearing call) The task floated `tomli`+`tomli-w` or a hand-roll using `tomllib`/`tomli` for reads. **Neither survives the real CI constraint**, which I discovered by reading `.github/workflows/ci.yml` (off-limits to edit): - The **test job** runs `pip install pytest cryptography` — **not** `requirements.txt`. - The **catalog-signature job** does `import bcc_core` with only `cryptography` installed. Therefore: (a) `bcc_core` must import with only `cryptography` present → **no top-level TOML import is possible**; and (b) any pip TOML dep would leave the new core **untested or red** on CI, since CI never installs it and I can't change the workflow. Even the task's option (b) fails: the `tomllib` read path is untested on the **3.10** runner (no stdlib `tomllib`). **Chosen:** stdlib-only, uniform across 3.10/3.12/3.13 — a lenient scalar reader + a **surgical line-editing writer** that rewrites only the one key it's asked to. Comments, formatting, ordering and unknown keys/tables round-trip untouched — strictly safer than parse→dict→reserialize, and `tomli-w` wouldn't comment-preserve anyway. Runtime deps stay exactly `PySide6` + `cryptography`. `requirements.txt` unchanged. ## What shipped ### #101 — hot-reload (PR #103) - **Core (unit-tested):** `sidecar_watch_paths` (file + its dir + doc path, de-duped), `sidecar_state_fingerprint` (hashable snapshot folding #91 sidecar status + #93 permission status), `sidecar_state_changed`. - **GUI (thin wiring):** `QFileSystemWatcher` over the selection's sidecar + BCC's own config, **debounced 300 ms**; **re-arm on every event** (atomic-rename replaces drop the inode); **focus-in fallback** via `changeEvent(ActivationChange)`; `recheck_advisories()` recomputes only warning labels (never field values, so it can't clobber unsaved edits); an external edit to BCC's own config shows a **non-destructive Reload banner** (confirm-on-dirty), never a silent overwrite. ### #102 — in-app sidecar editor (PR #104) - **Core (unit-tested):** `read_toml_section`, `toml_sections`, `set_toml_value`/`update_toml` (surgical, CRLF-safe, escaping, create-section, delete-on-None), `validate_sidecar_values` (enum/port on **managed** fields only; unknown keys pass through), `write_sidecar` (atomic + rotating backup + chmod 0600/0700 via #93; **never** `apply_servers`). Refactor: `_make_backup`/`write_config` now share `_atomic_write_text`; `_make_backup` keys on the file suffix (JSON naming byte-identical, so a `.toml` sidecar and `.json` config keep separate backup pools). - **GUI (thin wiring):** "Edit config…" button beside the sidecar advisory → `SidecarEditorDialog` (section picker, enum **pick-lists**, range-bounded port **spinner**, raw-TOML fallback). Save applies only changed managed fields on top of the raw text, validates, writes, then re-runs advisories through #101's path so they update live. ## GUI elements needing a human click-through CI has no PySide6, so none of the below is exercised by CI — I smoke-tested each headlessly (`QT_QPA_PLATFORM=offscreen`: dialog build, section detection, changed-field diff, surgical save with comment preserved + 0600 applied, and Edit-button visibility). Please verify interactively: **#101 — hot-reload** 1. Add an ssh-mcp server (`npx -y ssh-mcp --host=h --user=u`), select it. 2. In a terminal create the sidecar (macOS): `mkdir -p ~/Library/Application\ Support/ssh-mcp && printf '[server]\nhost="h"\n' > ~/Library/Application\ Support/ssh-mcp/config.toml`. Within ~1 s the **"arguments are inert"** advisory appears — no restart. 3. `chmod 644` that file → the **permission** advisory appears; `chmod 600` → it clears. 4. Edit BCC's own config JSON in another editor and save → the **Reload banner** appears; click **Reload from disk** (prompts if you have unsaved edits — confirm it does not clobber them). 5. Delete the `config.toml` → advisory clears. Alt-tab away and back to confirm the **focus-in fallback** also refreshes. **#102 — sidecar editor** 1. With the ssh-mcp server selected, an **"Edit config…"** button shows beside the advisory (also shows before the file exists, so you can create it). 2. Click it. With no file yet, the raw editor is empty and the section combo shows `(top level)`. Pick a `[server]` section (type `[server]` in raw or use the combo once a section exists), set `role = admin` via the pick-list, **Save**. 3. Confirm: the dialog reports the backup name + **0600**; `ls -l` shows `-rw-------`; the "args inert" advisory now shows (that's #101 firing). 4. Re-open, change `port` (spinner) and `auth` (pick-list), add a `# comment` in the raw text, **Save**. Re-open → the comment survived and the values changed (surgical round-trip). 5. Confirm invalid enum values are structurally impossible via the pick-lists. ## Known limitations (by design; none block — noted for click-through) - **Section-switch rebaselines the form.** Changing the section combo re-reads the pick-lists from raw text for the new section and resets the "changed since load" baseline. So pick-list edits made in section A, then abandoned by switching to section B *before Save*, are silently dropped. Fine for a first pass; the raw editor is always the source of truth. - **Raw-text edits bypass schema validation, intentionally.** Save validates only the *form-changed* managed fields, not raw-editor content — typing `auth = "sshkey"` directly in raw and saving is **not** rejected. This is the deliberate reconciliation of "reject out-of-enum" vs "preserve unknown keys": whole-file validation would block saves on a pre-existing invalid/unknown key the user never touched. The **pick-list guarantee is form-only**; the raw editor is the power-user escape hatch. - **First-pass editor scope.** One section targeted at a time; multi-profile editing is via the section picker + raw text. No comment-authoring UI (comments are preserved, not created). These are additive follow-ups if wanted. ## Nothing skipped / blocked Both issues landed CI-green. No irreversible actions taken: no merges, no force-push, no tags, no pushes to `main`, no touches outside `bcc_core.py` / `bcc.py` / `tests/` (`requirements.txt` untouched — zero new deps). `apply_servers` unchanged; `ClientSpec`/`ServerSpec` unchanged (added on top). ## Cardinal-rule check `apply_servers` still only writes `mcpServers`/`_disabledMcpServers`. The sidecar editor writes a **different file** through its **own** `write_sidecar`, which *reuses* `write_config`'s atomic-rename + `_make_backup` but never routes through `apply_servers` (there's an explicit test asserting no server keys leak into a sidecar write).
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: the_og/better-claude-config#94