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** | `