Skip to content

Refactor Phase 0: security pinning, UUID derivation fix, vendored JS, scripts/check - #1

Merged
remcovanmook merged 15 commits into
mainfrom
refactor/phase-0
Jul 16, 2026
Merged

remcovanmook merged 15 commits into
mainfrom
refactor/phase-0

Conversation

@remcovanmook

Copy link
Copy Markdown
Owner

Summary

Executes Phase 0 of the clean-slate refactor plan, plus two security fixes agreed during design review. Adds the plan itself (ARCHITECTURE.md, REFACTOR.md) to the repo. Also carries the three HTML-lint commits from fix/html-lint-warnings (branched from there).

Security fixes

  • Pin all provisioning-time connections to the stored host-key scan. ssh-new and ssh-user-rotate previously used StrictHostKeyChecking=accept-new, which accepted whatever host key was presented at deploy time — not necessarily the scanned one — and wrote it into the default ~/.ssh/known_hosts, silently trusting a provision-time MITM on the follow-on connection. Every connection these scripts make is now pinned via UserKnownHostsFile + StrictHostKeyChecking=yes against exactly the keys the host identity was derived from.
  • Host UUID derivation is now sha256("<key-type> <base64-key>") of the best scanned key. Previously the code hashed only the base64 column while ARCHITECTURE.md claimed the full scan line — both wrong. ⚠️ Pre-1.0 breaking change: existing host-uuid/ dirs won't match; re-provision (no migration, per plan).

Phase 0 (REFACTOR.md tasks 1–10)

  • W5 closed: install.sh performs no network fetches. xterm assets vendored at pinned @xterm/xterm@5.5.0 / @xterm/addon-fit@0.10.0 under lib/ui/vendor/ with SHA-384 sums; fetched via new scripts/vendor-update (accepts bytes only when unpkg and jsdelivr agree). socket.io-client dropped entirely — web terminal disabled until the Phase 1 native-WebSocket rewrite; other UI flows fall back to form POSTs.
  • scripts/check — single validation entry point (syntax / lint / vendor / tests / secrets stage modules), opt-in pre-push hook via scripts/install-hooks, thin CI wrapper .github/workflows/check.yml running the same script.
  • W9 handled: unrunnable ssh-rotate / ssh-template-rotate replaced with honest stubs (exit 1, point at Phase 5 rebuild).
  • ssh-new arg parsing de-duplicated with a regression test (tests/bash/test_ssh_new_usage.sh).
  • README: no-fetch install, disabled commands marked, Development section.

Test plan

  • scripts/check exits 0 on a fresh clone (syntax, vendor, tests, secrets; lint skips where shellcheck/ruff absent — CI runs them)
  • install.sh completed with curl/wget shadowed to fail and sandboxed $HOME — zero network attempts, vendored assets installed
  • Exit-gate greps: no undefined-helper references in bin/, no accept-new in bin/ or install.sh, bash -n clean on every script
  • UUID derivation exercised against sample scan data: type+key hashed, ed25519 preferred, ecdsa fallback, stable across hostname change
  • Web UI boots: serves vendored xterm byte-complete, 401 without auth, 404 on static-route path traversal

🤖 Generated with Claude Code

remcovm and others added 15 commits May 1, 2026 09:52
- nav-item divs: add role=button, tabindex=0, and onkeydown for
  keyboard accessibility (lines 31-33)
- th width attributes: replace deprecated HTML width= with style=width:
  (lines 56-59, 203)
- label elements: add for= attributes matching control ids throughout
  all modals (create, deploy, template create)
- deploy password input: add missing id= so label for= can reference it
- info modal host-keys table: add thead with Type/Fingerprint columns
- inline Jinja2 script block (lines 383/385): move template data into a
  <script type=application/json id=app-data> data island to eliminate
  IDE false-positive JS parse errors from double-brace Jinja2 syntax
- app.js: add initFromDataIsland() to read the JSON island at startup
  and populate window.identitiesList and window.xtermEnabled
- var → let/const throughout (const for values never reassigned)
- window → globalThis (window.location, window.open, window.close,
  window.addEventListener, window.xtermEnabled, window.identitiesList)
- String#replace(regex, ...) → String#replaceAll(str, ...) in escapeHtml
- deprecated document.domain → location.hostname in socket.io connect URL
- for (var i ...) over radios → for...of in triggerConnectSubmit
- handleTemplateCreate: remove redundant 'return false' statements;
  event.preventDefault() is sufficient, function is now void
- handleCreateSubmit: remove 'return false/true' pattern; allow native
  form POST by simply not calling preventDefault when xterm is disabled
- triggerConnect: remove empty if (response.ok) {} block; invert to
  if (!response.ok) for the error path only
- JSDoc added to all functions
- nav items: replace div role=button with real <button> elements; add
  button-reset CSS to .nav-item so rendering is unchanged
- flash message: darken colours from #cc0000/#ffcccc to #7a0000/#f5c6c6
  to meet WCAG AA minimum contrast ratio
- remove explanatory HTML comment block (flagged as commented-out code);
  the same explanation is present in the app.js JSDoc for initFromDataIsland()
…ed refactor plan

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…st-key scan

ssh-new and ssh-user-rotate previously connected with
StrictHostKeyChecking=accept-new, which accepted whatever host key was
presented — not necessarily the scanned one — and wrote it into the
default ~/.ssh/known_hosts, silently trusting a provision-time MITM on
the immediate follow-on connection.

Every connection these scripts make (ssh-copy-id, verification ssh,
post-provision exec ssh) now uses UserKnownHostsFile pointing at the
stored known_host_keys plus StrictHostKeyChecking=yes. ssh-new refreshes
the informational hostname column on successful re-verification so the
pin matches re-provisioning under a new alias; ssh-user-rotate rewrites
the column to the connect alias in a temp pin file.

The remaining accept-new in lib/ssh-ui.py is retired by the Phase 1
rewrite + Phase 4 bin/ssh-deploy (REFACTOR.md).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The code hashed only the base64 column; ARCHITECTURE.md claimed the full
"<host> <type> <base64>" line. Both were wrong: the agreed derivation is
sha256 over "<key-type> <base64-key>" — hostname column stripped so the
identity is pure key material, stable across hostname/IP changes.

Pre-1.0: existing host-uuid/<sha256> dirs no longer match; re-provision.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nstall-time fetch (W5)

install.sh no longer downloads anything: xterm assets are committed to
lib/ui/vendor/ with SHA-384 hashes recorded in SHA384SUMS. The vendored
bytes are the @latest snapshots the previous installer fetched on
2026-01-31 (legacy packages frozen at xterm@5.3.0 / addon-fit@0.8.0 —
see vendor/PROVENANCE); run scripts/vendor-update to fetch and pin
@xterm/xterm@5.5.0 with a dual-CDN byte-identical cross-check.

socket.io-client is dropped entirely, not vendored: Phase 1 replaces
flask_socketio with a hand-rolled RFC 6455 WebSocket and a browser-native
WebSocket client. Until then the web terminal is disabled
(xterm_enabled=False); all UI flows fall back to plain form POSTs.

The --install-xterm/--skip-xterm installer flags and the interactive
prompt are gone along with the download path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, thin CI wrapper

scripts/check runs staged modules from scripts/check.d/: syntax (bash -n,
py_compile), lint (shellcheck/ruff when installed, else [SKIP]), vendor
(SHA384SUMS verification incl. unrecorded-file detection), tests
(plain-bash test_*.sh always; bats/python suites when present), secrets
(token/private-key grep over tracked files).

Phase 0 lint severity is shellcheck -S error and ruff's runtime-error
subset; Phase 5 tightens to full severity per the REFACTOR.md exit gate.

scripts/install-hooks writes an opt-in pre-push hook that execs
scripts/check. CI (.github/workflows/check.yml) installs the optional
tools and runs the same script — no lint logic duplicated in YAML.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t stubs (W9)

Both scripts referenced helpers that were never defined anywhere
(validate_hostname, CONFIG_DIR, validate_template,
get_hosts_using_template, log) and failed immediately when run. Per
REFACTOR.md Phase 0 task 5 the preferred option is removal with a stub
that says so and exits 1 — keeping the codebase honest about what works.
Both are rebuilt from scratch, with bats coverage, in Phase 5.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, case arm)

The parser carried a duplicated --comment usage line, duplicated
NO_CONNECT/COMMENT_TEXT initialisation, and a dead duplicate --comment
case arm. One copy of each remains. tests/bash/test_ssh_new_usage.sh
(run by scripts/check tests) asserts --comment appears exactly once in
both the usage text and the parser.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…atus, Development section

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…fit@0.10.0

Fetched via scripts/vendor-update (dual-CDN byte-identical cross-check
between unpkg and jsdelivr); interim 5.3.0-era snapshots removed,
SHA384SUMS regenerated, index.html references and PROVENANCE updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@remcovanmook
remcovanmook merged commit 3c79d5c into main Jul 16, 2026
2 checks passed
@remcovanmook
remcovanmook deleted the refactor/phase-0 branch July 16, 2026 15:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants