feat(desktop): let the agent inspect the desktop app it's developing - #73121
Merged
Conversation
The renderer is a Chromium page, and apps/desktop already carries a whole
CDP toolkit for it — scripts/eval.mjs, scripts/perf/lib/cdp.mjs with its
shared SELECTORS map, and the diag-*/probe-* family. None of it can
attach to `hgui` or `npm run dev`, because neither passes
--remote-debugging-port. The only launcher that opens one is
`npm run perf:serve`, which is a separate isolated instance rather than
the app you're looking at.
Add HERMES_DESKTOP_CDP_PORT. When set, the shell opens a CDP port on
loopback so that existing tooling can read the live DOM: computed
styles, geometry, which rule actually won.
Three independent gates, all required, resolved by a pure function in
electron/dev-cdp.ts so the policy is testable without an Electron app:
1. not packaged — a shipped build never opens the port, and this is
checked first so no env combination can talk it into doing so;
2. HERMES_DESKTOP_DEV_SERVER present — an unpackaged `electron .`
against dist/ is how the packaged app gets smoke tested, so it
behaves like the packaged app here;
3. the port explicitly requested and a valid integer.
Default `npm run dev` is unchanged and silent: no port, no nag. An
opt-in that gets refused always logs why, so nobody loses an hour
wondering what isn't listening.
The address is pinned to 127.0.0.1 rather than left to Chromium's
default, and is deliberately not configurable — there's no reason to
expose a renderer debugger off-host and offering the knob invites
someone to try.
scripts/eval.mjs hardcoded :9222 and threw a raw ECONNREFUSED stack when
nothing was there. It now honours the same variable and explains itself.
Contributor
૮ >ﻌ< ა ci reviewran on d76d083 ℹ️ InfoDesktop E2E visual evidence · View test artifacts · View job1 visual diff. inline evidence upload failed. Failed to upload diff-665a0833239e-onboarding-overlay-diff.png with gh image (exit code 1): Error uploading /home/runner/work/_temp/e2e-evidence/diff-665a0833239e-onboarding-overlay-diff.png: step 0 (get upload token): uploadToken not found on repo page — do you have write access to NousResearch/hermes-agent? (or, if NousResearch enforces SAML SSO, authorize at https://github.com/orgs/NousResearch/sso) |
Gating this behind an opt-in was the wrong call. A dev server already executes arbitrary local JS — vite's module graph, every postinstall in node_modules — so a loopback debugging port does not meaningfully widen what a `npm run dev` session can already do, and `perf:serve` has opened one unconditionally all along. Requiring the variable also defeated the point: the tooling exists to be reached for mid-task, and a capability you must remember to enable before launching is one you don't have when you need it. So the port opens on 9222 — the same port scripts/eval.mjs and scripts/perf/lib/cdp.mjs already default to — for any dev-server run. HERMES_DESKTOP_CDP_PORT stops being an on-switch and becomes an override: a different port, or `off` to disable. The hard gate is unchanged and still checked first: a packaged build never opens the port, and no env value talks it into it. Neither does an unpackaged `electron .` against dist/, which is how the packaged app gets smoke tested. Refusals only log when they contradict something the developer asked for (a typo'd port, an explicit `off`). Packaged and dist runs are closed by design and stay quiet.
OutThisLife
enabled auto-merge
July 28, 2026 04:56
OutThisLife
disabled auto-merge
July 28, 2026 04:58
The port is only half of it. Ships the skill that tells the agent the capability exists, when reaching for it beats reading .tsx, and how not to hurt the user's running app while using it. Lands in skills/software-development/ next to node-inspect-debugger, which covers the same protocol for Node/perf work — this one is the DOM/CSS half. Load-bearing parts: don't relaunch or kill the user's app to get a port (a mid-serve kill nukes Chromium's socket pool and the fallout gets blamed on the last CSS edit); never dump the whole DOM into context; prefer the maintained SELECTORS map to invented querySelectors; and CDP answers factual questions only — whether it *looks* right is still the user's call.
randlee
pushed a commit
to randlee/hermes-agent
that referenced
this pull request
Aug 11, 2026
…v-cdp-port feat(desktop): let the agent inspect the desktop app it's developing
swissly
added a commit
to swissly/hermes-agent
that referenced
this pull request
Aug 12, 2026
The skill was added in NousResearch#73121 without a risk field. 38/39 bundled skills carry risk; this one is a read-only inspection skill (CDP DOM reads), so classify it READ to satisfy frontmatter validation.
33hodl
pushed a commit
to 33hodl/hermes-agent
that referenced
this pull request
Aug 12, 2026
…v-cdp-port feat(desktop): let the agent inspect the desktop app it's developing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hermes develops Hermes Desktop but cannot see it. UI questions — did that change take effect, why is this element still the wrong colour, what does this token compute to — get answered by reading
.tsxand inferring, or by asking the user to look. The renderer is a Chromium page; it was one command-line switch away from being readable.How it works
The switch.
app.commandLine.appendSwitch('remote-debugging-port', N)makes Chromium start a debugging server. It has to be set at module scope (main.ts:280) — Chromium parses its command line during init, which happens between module evaluation andapp.whenReady()(line 11378). Appending the switch inside the ready handler is a silent no-op. That's why this sits with the GPU/compositing switches rather than anywhere more obvious.What the port serves.
GET /json/listenumerates every debuggable target (each BrowserWindow, plus devtools windows) with awebSocketDebuggerUrl. Open that socket and you're speaking CDP:Runtime.evaluatefor expressions,DOM/CSSfor structure and computed styles,Input.dispatchKeyEventfor synthetic input.scripts/perf/lib/cdp.mjsalready wraps all of it — request/response correlation byid, event listeners, promise-awareeval.Why 9222. 24 scripts under
scripts/already hardcode or default to it. Matching that default is what makes the toolkit work with zero configuration rather than needing every script touched.Target selection matters. A running desktop has several page targets — main window, pet overlay (
?win=overlay), quick entry (?win=quick).CDP.connect({ match: '5174' })filters on the dev-server URL to get the main window; withoutmatchyou attach to whichever target enumerates first.How the gate works
electron/dev-cdp.tsis a pure function over{ env, isPackaged, devServer }. Pure because the alternative is testing a gate by launching Electron, which is slow enough that nobody does it, and the packaged branch is the one that must never regress.Packaged is checked first, and it's stronger than it looks:
scripts/bundle-electron-main.mjsesbuild-definesprocess.env.HERMES_DESKTOP_IS_PACKAGEDto the literaltruefor every non---devbundle. So in a production bundle that expression is constant-folded at build time — the packaged check isn't reading the environment, it's a baked-intruethat survives an unpackagedelectron .againstdist/. Setting the env var can only ever turn the gate on, never off.The dev-server check is the second discriminator.
HERMES_DESKTOP_DEV_SERVERis only set bynpm run dev/hgui, so its presence distinguishes a source-tree run from adist/-loading one. Thedist/case is how the packaged app gets smoke tested, so it should behave like the packaged app.Everything else opens on 9222.
HERMES_DESKTOP_CDP_PORTis an override — a different port, oroff/0/false/no. Out-of-range and non-integer values are refused rather than silently coerced (Number('92 22')isNaN, butNumber('')is0, which is why the empty case is handled before the numeric parse).Refusals log only when they contradict something explicitly requested — a typo'd port, an explicit
off. Packaged anddist/runs are closed by design and stay quiet, because a warning on every packaged launch is noise nobody can act on.Why on by default
A dev server already executes arbitrary local JS: vite's module graph, every postinstall in
node_modules. A loopback CDP port doesn't widen that, andperf:servehas opened one unconditionally since it was written. The address is pinned to127.0.0.1explicitly rather than relying on Chromium's default, and isn't configurable — off-host renderer debugging has no use case here, and the knob would only invite someone to try.The skill
skills/software-development/inspecting-hermes-desktop-dom— the port is useless if the agent doesn't know it exists. Sits besidenode-inspect-debugger, which covers the same protocol for Node/perf; this is the DOM/CSS half.The load-bearing content is what goes wrong:
ERR_NETWORK_CHANGEDgets blamed on whatever was edited last. Launch an isolated instance (--user-data-dirdodges the single-instance lock, separateHERMES_HOMEkeeps it off real sessions).getComputedStyleon the real node and walk ancestors before editing call sites. No class on the node means the value is inherited and sweeping usages won't fix it.HERMES_HOMEhas no backend, so a probe instance logsECONNREFUSEDand can exit on its own. Chromium'sDevTools listening on ws://…line is the proof the port bound; don't read a self-exited probe as a broken gate. (Hit this during verification.)Also
scripts/eval.mjshardcoded:9222and threw a rawECONNREFUSEDstack when nothing was listening. Honours the override now, falls back to any page target when the dev-port match misses, and explains itself on failure.Verification
Gate: 11 unit tests (
vitest run --project electron electron/dev-cdp.test.ts). Skill frontmatter run through the repo's own_validate_frontmatter+extract_skill_description— first draft truncated at the 57-char index budget and lost its trigger word, so it was shortened;tests/tools/test_skill_manager_tool.py129 passed.Runtime, real Electron, isolated
--user-data-dir+HERMES_HOMEso a livehguiwas never touched:DevTools listening on ws://127.0.0.1:9222/…;lsofshowsTCP 127.0.0.1:9222 (LISTEN)— loopback, not*:9222;scripts/eval.mjsreturned{"title":"Hermes","nodes":216}HERMES_DESKTOP_CDP_PORT=offrenderer debugging disabled by HERMES_DESKTOP_CDP_PORT.HERMES_DESKTOP_IS_PACKAGED=1+=9222DevTools listening, nothing boundtsc --build tsconfig.electron.jsonand eslint clean. 413 insertions, 2 deletions across 6 files.