From 78840510610062b0243000704f1ec99a2817c731 Mon Sep 17 00:00:00 2001 From: Thomas Bekkers <261084914+thomasbek3@users.noreply.github.com> Date: Mon, 7 Sep 2026 01:05:47 -0400 Subject: [PATCH] final-review adjustments (plugins): keep RSA pins across probe/reconnect, vendor/ path in README, precise noVNC errors computer-viewer: every persist that spreads an endpoint now starts from liveEndpoint() (which also falls back to a settings lookup by id), and the backoff reconnect resolves the live endpoint at fire time, so a pin stored on first contact survives probe success and auto-reconnect instead of being overwritten by a pre-pin copy. A pinned computer that presents no key is rejected. loadVendoredRFB tries vendor/novnc-rfb.mjs first, then the legacy next-to-plugin.js location, both hash-checked; a tampered candidate stops the search. connect() error bodies name the actual reason (hash mismatch vs could not be loaded vs missing plus CDN blocked). README paths corrected. bot-sections: PLUGIN_CSS rename so cssAttr reaches CSS.escape. task-dock: observer watches aria-label and drops the dead chat-surface attributes. 9 new tests (58 JS total). Manifest regenerated. Co-Authored-By: Claude Fable 5.1 --- MANIFEST.sha256 | 6 +- bot-sections/plugin.js | 4 +- bot-sections/plugin.test.mjs | 39 +++++- computer-viewer/README.md | 25 ++-- computer-viewer/plugin.js | 97 +++++++++---- computer-viewer/plugin.test.mjs | 236 ++++++++++++++++++++++++++++++-- task-dock/plugin.js | 12 +- task-dock/plugin.test.mjs | 29 +++- 8 files changed, 378 insertions(+), 70 deletions(-) diff --git a/MANIFEST.sha256 b/MANIFEST.sha256 index eaa835a..d6b77c7 100644 --- a/MANIFEST.sha256 +++ b/MANIFEST.sha256 @@ -1,4 +1,4 @@ -ec106912cacb7af32006cacf01f2c1f6286d3a22855e27445db663a577a72871 bot-sections/plugin.js +fc38ac67cd7783bbc701a229cbfa4b0be6b24b1ea74a286571249c7b252e431f bot-sections/plugin.js abc2ed7fa6b6e317827b3ffe58d4d686c6169b4f9559c040730d64d8da901325 bubble-mode/plugin.js 8437e28c1a15ded71ab48b89e047a1481a1319d6d5f9f6fb6eefc7d8c06758e0 computer-viewer/agent-plugin/orgo-computer/__init__.py caaaf1e751ddcba1d4b216ab18bd4077d18c4c9c8e85915d45bdbf746a5f95fc computer-viewer/agent-plugin/orgo-computer/plugin.yaml @@ -12,8 +12,8 @@ b8ac63b1b052a029b46ac9bda394215e7b4c2268506c9becd86eb49190a9faf6 computer-viewe 2f0c0b90e7e5c9db0187d6693d0c0515c442c3ae17aaa590520afbb7e22650a6 computer-viewer/hiperf-linux.sh e3217fd93aa6c78bab712d376793a1f052740f1a6f8bfa309c63d5087e7a3632 computer-viewer/hiperf-mac.sh 37502a54eb33646ff4eb76866f0ef2d1bb86c1ab87ce8bb85c6f7e249950447f computer-viewer/hiperf-windows.ps1 -093aee168eb589a88f16a7402654c038b6ebca1aa36e10ea75df811c00cc34d3 computer-viewer/plugin.js +9c4f76c9bb89e4cbf49b1c04fb02a823238c161712b88fff600aa36d94b514f3 computer-viewer/plugin.js 08b527c943eb410ffa1a35d7d14c018d9eba22363150c3dc35a70327f1e88a28 computer-viewer/vendor/novnc-rfb.mjs -0aceb385ca7c9f1d305b2b0d7daa0f177b3a778c60beb6e901a2f0ba5622654c task-dock/plugin.js +fdc05be2a112a2fece0b0db4998c13981b1fc5d15ebe372760e9f4d6c033d99d task-dock/plugin.js f3e9050ac9fd66008cf775208ddcb28cc3dd44c80c268b2cbd53244f0a270429 texting-style/__init__.py edc6cd000b606b21499ad175bc44d1375d38cec5a1ace89c5198d85726e828f9 texting-style/plugin.yaml diff --git a/bot-sections/plugin.js b/bot-sections/plugin.js index ee86afb..3f9ab83 100644 --- a/bot-sections/plugin.js +++ b/bot-sections/plugin.js @@ -112,7 +112,7 @@ function sectionLadder() { return ladder } -const CSS = /* css */ ` +const PLUGIN_CSS = /* css */ ` body.hermes-bot-sections [${LIST_ATTR}] { display: flex; flex-direction: column; @@ -1271,7 +1271,7 @@ function injectStyle() { if (document.getElementById(STYLE_ID)) return const el = document.createElement('style') el.id = STYLE_ID - el.textContent = CSS + el.textContent = PLUGIN_CSS ;(document.head || document.documentElement).appendChild(el) } diff --git a/bot-sections/plugin.test.mjs b/bot-sections/plugin.test.mjs index d54ae0e..e2ff35e 100644 --- a/bot-sections/plugin.test.mjs +++ b/bot-sections/plugin.test.mjs @@ -7,8 +7,19 @@ const pluginPath = new URL('./plugin.js', import.meta.url) const SOURCE = fs.readFileSync(pluginPath, 'utf8') const HOOKS = - '\nglobalThis.__testHooks = { applyAssignData, applyAssignFileText, findBotsPaneRoots,' + - ' sectionForKey, sectionLadder, getOverrides: () => overrides, getCustomSections: () => customSections }\n' + '\nglobalThis.__testHooks = { applyAssignData, applyAssignFileText, findBotsPaneRoots, cssAttr,' + + ' HEADER_ATTR, sectionForKey, sectionLadder, getOverrides: () => overrides,' + + ' getCustomSections: () => customSections }\n' + +/** The parts of CSS.escape a section name can actually hit. */ +function cssEscape(value) { + return String(value).replace(/[^\w-]/g, ch => '\\' + ch) +} + +/** Undo CSS string escaping, the way a selector parser would. */ +function cssUnescape(value) { + return String(value).replace(/\\(.)/g, '$1') +} function classList(...names) { const set = new Set(names) @@ -28,9 +39,15 @@ function el({ className = '', textContent = '', classes = [], parentElement = nu function load({ spans = [] } = {}) { const warnings = [] + const escaped = [] const context = { __host: { state: {} }, - CSS: { escape: value => value }, + CSS: { + escape: value => { + escaped.push(value) + return cssEscape(value) + } + }, console: { warn: message => warnings.push(message) }, document: { body: { classList: { add() {}, contains: () => false, remove() {} } }, @@ -50,7 +67,7 @@ function load({ spans = [] } = {}) { .concat(HOOKS) vm.runInNewContext(source, vm.createContext(context), { filename: pluginPath.pathname }) - return { ...context.__testHooks, warnings } + return { ...context.__testHooks, warnings, escaped } } test('a valid bot-sections.json creates its sections and assigns its bots', () => { @@ -164,3 +181,17 @@ test('findBotsPaneRoots() falls back to the grandparent when no flex column is f assert.equal(roots.length, 1) assert.equal(roots[0], grandparent) }) + +test('a section name with a quote and a bracket still matches its own header', () => { + const kit = load() + const name = 'a"b]c' + + const selector = `:scope > [${kit.HEADER_ATTR}="${kit.cssAttr(name)}"]` + + // The module used to shadow the CSS Web API with its own stylesheet + // constant, so CSS.escape was never reached and the fallback regex ran. + assert.deepEqual(kit.escaped, [name]) + const quoted = selector.match(/="(.*)"\]$/) + assert.ok(quoted, selector) + assert.equal(cssUnescape(quoted[1]), name) +}) diff --git a/computer-viewer/README.md b/computer-viewer/README.md index 4292bfd..626e310 100644 --- a/computer-viewer/README.md +++ b/computer-viewer/README.md @@ -86,7 +86,7 @@ viewed through the pane's noVNC connection.

``` plugin.js the entire desktop plugin (single ESM file) -vendor/novnc-rfb.mjs bundled noVNC 1.7.0 RFB (installer copies it next to plugin.js) +vendor/novnc-rfb.mjs bundled noVNC 1.7.0 RFB (installer copies it to vendor/ on the target) connect-mac.sh one-paste VNC bridge setup for a Mac connect-windows.ps1 same for Windows (TightVNC/UltraVNC + websockify + virtual display) connect-linux.sh same for Linux (x11vnc / wayvnc + websockify) @@ -373,23 +373,24 @@ be `computer-viewer`. ```bash # macOS / Linux (default profile) -mkdir -p ~/.hermes/desktop-plugins/computer-viewer +mkdir -p ~/.hermes/desktop-plugins/computer-viewer/vendor cp plugin.js ~/.hermes/desktop-plugins/computer-viewer/plugin.js -cp vendor/novnc-rfb.mjs ~/.hermes/desktop-plugins/computer-viewer/novnc-rfb.mjs +cp vendor/novnc-rfb.mjs ~/.hermes/desktop-plugins/computer-viewer/vendor/novnc-rfb.mjs cp connect-mac.sh connect-linux.sh hiperf-mac.sh hiperf-agent.py ~/.hermes/desktop-plugins/computer-viewer/ chmod +x ~/.hermes/desktop-plugins/computer-viewer/connect-*.sh ~/.hermes/desktop-plugins/computer-viewer/hiperf-*.sh ``` -The kit installer places `novnc-rfb.mjs` next to `plugin.js`. The plugin -verifies that file's SHA-256 before loading it, and only then falls back to -the CDN. +The kit installer places `novnc-rfb.mjs` in `computer-viewer/vendor/`. The +plugin verifies that file's SHA-256 before loading it, and only then falls back +to the CDN. A copy left next to `plugin.js` by an older release is still read +(and hash-checked) if `vendor/` has none. Named Hermes profile: ```bash -mkdir -p ~/.hermes/profiles//desktop-plugins/computer-viewer +mkdir -p ~/.hermes/profiles//desktop-plugins/computer-viewer/vendor cp plugin.js ~/.hermes/profiles//desktop-plugins/computer-viewer/plugin.js -cp vendor/novnc-rfb.mjs ~/.hermes/profiles//desktop-plugins/computer-viewer/novnc-rfb.mjs +cp vendor/novnc-rfb.mjs ~/.hermes/profiles//desktop-plugins/computer-viewer/vendor/novnc-rfb.mjs cp connect-mac.sh connect-linux.sh hiperf-mac.sh hiperf-agent.py ~/.hermes/profiles//desktop-plugins/computer-viewer/ chmod +x ~/.hermes/profiles//desktop-plugins/computer-viewer/connect-*.sh ~/.hermes/profiles//desktop-plugins/computer-viewer/hiperf-*.sh ``` @@ -398,7 +399,7 @@ Windows (typical): ```text %LOCALAPPDATA%\hermes\desktop-plugins\computer-viewer\plugin.js -%LOCALAPPDATA%\hermes\desktop-plugins\computer-viewer\novnc-rfb.mjs +%LOCALAPPDATA%\hermes\desktop-plugins\computer-viewer\vendor\novnc-rfb.mjs %LOCALAPPDATA%\hermes\desktop-plugins\computer-viewer\connect-windows.ps1 ``` @@ -734,7 +735,7 @@ The address field picks a mode for you. These names only appear under | Mode | How it connects | When to use | |---|---|---| -| **WebSocket** (default) | Loads the vendored noVNC 1.7.0 (`RFB`) the installer places next to `plugin.js`, after a SHA-256 check. Falls back to jsDelivr then `esm.sh` only if that file is **missing, truncated, or unreadable**; those CDN copies are unverified. A file that is present but **fails the hash check, or fails to import**, is treated as tampering: the pane stops with **Viewer file failed verification** and no CDN copy is loaded. | Full controls: scale, view-only, clipboard, Ctrl+Alt+Del, screenshot. | +| **WebSocket** (default) | Loads the vendored noVNC 1.7.0 (`RFB`) the installer places in `computer-viewer/vendor/`, after a SHA-256 check. Falls back to jsDelivr then `esm.sh` only if that file is **missing, truncated, or unreadable**; those CDN copies are unverified. A file that is present but **fails the hash check, or fails to import**, is treated as tampering: the pane stops with **Viewer file failed verification** and no CDN copy is loaded. | Full controls: scale, view-only, clipboard, Ctrl+Alt+Del, screenshot. | | **Iframe** | `