feat(webapp): serve Desktop renderer in browsers - #93508
BearHuddleston wants to merge 76 commits into
Conversation
25fec8b to
e1e8c97
Compare
|
Awesome! Please can this land? I'd really like to use this. |
f0566c0 to
940fd2c
Compare
ce3c499 to
fdf2f61
Compare
andrexibiza
left a comment
There was a problem hiding this comment.
Reviewed exact head d28137e4c1d2923445fe00bf75bdd341f3200607 against live main@68518c1f9bca11d9f5dbdf59ecf7e024cce057ba. This is currently a clean four-commit child of main, open/non-draft/mergeable. I inspected the browser bridge, authenticated API/WS/PTY boundary, profile-scoped uploads, profile lifecycle/tombstone changes, SessionDB guards, TUI session retirement, the profile deletion race suite, existing discussion, and the current related decomposition/security graph. There were no formal reviews on this exact head when I started.
There is a lot worth preserving here. The browser bridge stays same-origin, auth-gated browser WebSockets use one-time tickets, non-loopback Webapp startup fails closed without configured auth, host-shell env construction scrubs secrets, uploaded browser files are staged as bytes rather than treating client paths as host paths, and HTML preview fallback is isolated in a sandboxed iframe. The deletion work is also unusually thorough: it publishes the tombstone before teardown, avoids recreating named-profile parents from SessionDB/config/attachment paths, retires in-process sessions/agent builders, checks external file holders, and uses positive (pid, create_time) process identity instead of guessing from argv. The new race suite is doing real adversarial work rather than merely asserting the happy path. 🚀
I do have two merge blockers on the submitted object.
P1 — the deletion tombstone fences a name/path, not a profile incarnation, so same-name recreation re-authorizes stale actors
The new guard is durable only while profiles/<name> is absent/tombstoned. The implementation deliberately supports recreating the same name:
hermes_constants.profile_deletion_marker_path()derives one marker from the profile path/name only.profiles.create_profile()publishes the new directory, then calls_clear_profile_deletion_marker(profile_dir)and_allow_in_process_profile_resources(profile_dir).tui_gateway.server.allow_profile_home()re-admits by deleting that resolved path from_retired_profile_homes.hermes_state.SessionDB.__init__()rejects a named home only from current path/marker availability before openingstate.db; it carries no expected profile generation/incarnation.
The test test_explicit_recreate_clears_profile_deletion_tombstone explicitly pins the re-admission behavior. test_config_memo_never_crosses_profile_directory_generation correctly notices one incarnation problem for the config initialization memo and compares directory identity, but that stronger identity is not propagated to the mutation surfaces above.
That leaves a deterministic ABA shape: actor A retains profiles/worker without holding an open file at deletion time; generation A is deleted; worker is explicitly recreated as generation B; the same marker/path is cleared and re-admitted; A resumes and a fresh SessionDB(profile_dir / "state.db") or another path-qualified writer now passes the guard and can mutate generation B. The current retirement + FD census protects actors it can observe during teardown, but it cannot turn a reusable path into incarnation identity for delayed/suspended/external actors that hold no file during the census.
This is exactly the class tracked by #90145: stable logical identity and generation/incarnation are separate dimensions, and stale actors must compare the generation they observed at the mutation boundary rather than reconstruct authority from a reusable coordinate.
Required repair: give each named profile a durable, never-reused profile_incarnation (opaque token is fine), capture it when a session/writer obtains profile authority, and compare the expected incarnation at the real DB/file/config/session publication boundary. Deletion tombstones must retire a specific incarnation; same-name recreation must mint a new one. Clearing a name tombstone must not make old-generation proof valid again.
Please add one vertical regression that forces the actual interleaving: actor A captures profile generation 1 and pauses before opening/writing; delete it; recreate the same profile name as generation 2; resume A and prove its DB/file mutation is refused and generation 2 is untouched; then prove a generation-2 actor succeeds. That closes the class rather than relying on a complete census of possible stale actors.
P1 — this feature adds substantial new business/security behavior back into two active godfile decomposition targets
This head directly adds new host-shell/auth/WS/profile/upload behavior to hermes_cli/web_server.py at hunks around line 20,000 and new session/profile lifecycle behavior to tui_gateway/server.py at hunks beyond line 12,000. Both are explicit active decomposition targets: #78628 owns web_server.py; #78630 owns tui_gateway/server.py. The repository's hard invariant is not "don't create a new file above 2K" — these godfiles are to be sharded and never grown/restored as business-logic owners.
There is already live ownership for the exact neighborhoods this PR expands:
- #79125 owns the managed-files/upload extraction seam from
web_server.py. - #79778 owns the console/SPA-mount/WS-auth extraction neighborhood.
- #79259 / #79672 and the other #78630 slices own TUI session/server decomposition, including session/CWD state.
Landing this feature into the monoliths first makes those open extraction objects stale and forces them to re-extract newly added authority code after the fact. That is the opposite merge order from the KILL LOCK.
Required repair: compose with the existing decomposition ownership before landing. Either land/retarget the applicable #78628/#78630 slices and restack this feature onto the extracted seams, or move this PR's new Webapp/host-terminal/profile-session business logic into focused sub-2K owners now and explicitly reconcile the existing shard PRs with preserved contributor credit. Please do not solve this as a textual conflict: the upload, WS-auth, profile/session and monkeypatch/seam ownership contracts need a semantic merge.
Graph / ownership notes
- #85604 (
adybag14-cyber) is the browser-bridge/build predecessor; this PR correctly preserves that lineage while dropping its arbitrary-shell design. - #61171 (
seagpt) retains the explicithermes webappproduct-boundary lineage; #60829 is adjacent UX history. - #92731 / #90006 remain the connection-owner Bot-routing lineage; I found no second resolver introduced here.
- #90145 is the direct generation/ABA architecture interlock for the profile lifecycle blocker above; #90866 and #93943 are the broader proof-carrying / nothing-ambient-survives laws.
- #94850 is complementary Web-server/CSP/signed-download hardening, not a duplicate. It overlaps
web_server.pyand browser file/media surfaces; whichever lands second should preserve its CSP/session-token hardening while this feature broadens the authenticated browser surface to shell/Git/files. - #78628/#78630 and their extraction PRs are structural owners, not competing feature implementations. Preserve those contributor lineages when composing rather than closing them as obsolete simply because this branch touched the same monoliths.
Exact-head evidence
On d28137e4…, Docker 33012180980 and Nix 33012180983 are green. CI 33012183818 is still in progress at review time. The previous head's JS/TS failure is useful history but does not certify or condemn this head; the test-only d28137e4 fixture repair needs its own completed exact-head CI receipt before merge.
The browser-hosted Desktop direction is strong, and the host-capability boundary is much better than the abandoned arbitrary-shell shape. The remaining work is concentrated at two architecture boundaries: make profile identity survive deletion/recreation as an incarnation, and land the new behavior into the owners that are already replacing the monoliths. Once those are closed and the exact final commit is fully green, this is a much cleaner foundation for browser-hosted Hermes. 🚀
andrexibiza
left a comment
There was a problem hiding this comment.
Exact-head evidence update for the review above: CI 33012183818, Docker 33012180980, and Nix 33012180983 have now all completed successfully on the unchanged d28137e4c1d2923445fe00bf75bdd341f3200607 head.
That closes the hosted-evidence gap recorded in review 5034982785. It does not change either code/architecture blocker: profile authority still needs an incarnation fence across same-name recreation, and the new Webapp/profile-session behavior still needs composition onto the active #78628/#78630 shard topology before landing.
752b001 to
60c5c8a
Compare
|
@andrexibiza Thanks for the detailed review. Both blockers are addressed on exact head 1. Same-name profile recreation / ABAThe path is no longer the authority token:
The vertical regression performs create(A) → retain A session → delete(A) → recreate(B): A cannot create B's A late bounded review correctly caught that my first response still relied on check-before/post-write validation at two pathname binders. This exact head closes that check/use window with the shared lease rather than merely detecting the generation change afterward. It also converts lifecycle-lock acquisition timeout to the existing fail-closed Primary owners: 2. Web/TUI decomposition and contributor creditI re-read #78628, #78630, #79125, #79778, #79259, and #79672 before changing structure. The four cited shard PRs are still open/unmerged, are thousands of commits behind current I therefore did not copy/cherry-pick stale shard code into this feature or claim its authorship. There is no borrowed code whose contributor trailer needs to be reassigned. Instead, the branch now removes its own new business logic from the monoliths:
These feature-specific owners do not supersede the generic decomposition proposals and can be composed if maintainers land those proposals later. Original feature lineage remains credited to @adybag14-cyber and @seagpt. During rebase, upstream independently landed the typing-sync CI repair as Verification
Exact-head GitHub CI settled at 24 passed, 11 skipped, 0 failed across CI |
andrexibiza
left a comment
There was a problem hiding this comment.
Re-reviewed exact head 60c5c8aac268115d1a70792326970dddc2139ebb after the blocker response and re-read the actual mutation boundaries rather than treating the response as evidence by itself.
The two blockers from my d28137e4… review are materially closed on this object.
Closed — profile delete/recreate ABA
This is now an incarnation fence, not a path fence:
hermes_cli/profile_incarnation.pygives named-profile generations a persisted 128-bit token and uses atomic publication for compatibility backfill;- create/import mint a fresh incarnation while rename preserves the existing lifecycle object;
ProfileLifecycleFencekeeps retirement as(resolved home, incarnation)and does not accidentally re-authorize generation A when generation B is admitted at the same path;- sessions/deferred builders/compute-host/profile DB openers carry the captured token;
SessionDBchecks the named-profile generation before open and again immediately after_connect_tracked_db(), before WAL/PRAGMA/schema work, so delete/recreate cannot win the preflight→open race and turn a stale opener into a writer on the replacement;- the vertical regression proves generation A cannot create generation B's
state.dbor attachment while a generation-B actor succeeds.
That is the proof-carrying boundary I asked for. The original #90145-style ABA defect is not present in the submitted head.
Closed — new policy living in the godfiles
The new business/security ownership is no longer concentrated back into the monoliths. hermes_cli/web_host_terminal.py owns host-terminal policy/process construction and tui_gateway/profile_lifecycle.py owns generation fencing/session retirement, while web_server.py / server.py retain compatibility/wiring seams. The new owners are focused and comfortably below the 2K ceiling. I also rechecked the lineage decision: this branch did not copy the still-unmerged decomposition shards, and the original browser/webapp contributor trailers remain intact.
Exact-head evidence
60c5c8aa… has a settled exact-head GitHub run: 24 success, 10 skipped, 1 neutral, 0 failed, 0 pending, including Python, native Windows/macOS, JS/TS, Docker amd64/arm64, Nix, and All required checks pass. The submitted five-commit object is internally coherent; I did not find a new code/security blocker in the 80-file delta.
Landing-edge gate — current main moved in the same authority neighborhood after this green receipt
I am not calling this production-ready against stale base evidence, because live main has advanced again. Current main is now db127f75021fe74ee4c8f32433a3db12bd3c10dc, whose fix(desktop): session rows are identified by (profile, id) changes Desktop resume identity from bare session ID to (profile, id) and makes the clicked row's own (connection, profile) the resume owner before navigation.
That is not an unrelated tick for this PR. This feature serves the actual Desktop renderer in the browser and broadens profile/session/Bot routing through the browser bridge. The new main commit is therefore the other side of the same routing shape: profile-qualified stored-session identity must remain exact when this browser host is composed on top of it.
Required final production receipt: compose/rebase this exact feature onto current main@db127f75… (or whatever main is at the actual landing edge), preserve the new (profile, id) / resume-owner semantics, then rerun the full required CI plus the real browser-host smoke on that resulting exact head. The smoke should include two profiles containing the same stored session ID and prove that clicking/resuming each row stays with that row's own profile/connection through the browser-host bridge.
If that exact landing object is green and the twin-session browser regression passes, I have no remaining architecture objection. I want this shipped; I just do not want the production object certified one main-generation before a directly adjacent identity fix.
60c5c8a to
695f99e
Compare
Integrate main through 6f3e630 without rewriting contributor history. Preserve scoped gateway PID cleanup and compose legacy profile aliases with the browser profile lifecycle fences. Existing reserved-name profiles remain readable; retired or tombstoned aliases never route to default. Add configuration and database routing regressions.
Give gateway lookalikes their owning profile in argv and the environment, and wait for the runtime lock before testing stop behavior. Exercise the current invalid-header probe for both zeroed and non-SQLite databases during profile deletion. Reuse isolated_update_runtime in the HEAD-movement tests so module purging cannot discard discovery mocks and scan unrelated live gateways.
Merge main at 3b45681, retaining browser link handling and prompt display provenance alongside connector authorization and user-activity tracking. Preserve profile retirement guards and main's shared SessionDB cleanup. Exercise native HUD capability in the new titlebar placement tests and cover browser behavior for both positions.
Resolve profile lifecycle and state ownership conflicts, keep browser pasted-text attachments working, and align the search test with pooled database reads.
Preserve profile lifecycle fencing while integrating upstream session handling and atomic credential writes. Update gateway contracts for browser uploads and message provenance, refresh lock metadata, and reconcile the affected tests.
Preserve browser session reconciliation while adopting the renamed resume result type. Finish the bot-chat test's queued turn before the shared gateway fixture is reset.
Merge origin/main at 416a817 without conflicts. Preserve browser-only context-menu cancellation alongside main's profile-rail menu action.
Merge origin/main at 98f758a into PR NousResearch#93508 without rewriting the feature branch history. Resolve each conflicted path: - apps/desktop/src/app/session/hooks/use-session-actions/utils.ts: compose main's typed synthetic display metadata with the PR's runtime provenance and persisted-notice reconciliation. Keep hidden rows hidden. - apps/desktop/src/lib/desktop-git.ts: retain the shared browser/remote REST bridge and main's removal of the obsolete PR-comment helper; remove its auto-merged stub and assertion. - hermes_cli/main_web_build.py: retain main's shared input enumeration and directory-aware ignore pruning while hashing the PR's additional shared renderer source tree too. - tui_gateway/contracts/sessions.py: expose both user_originated and typed display metadata on inflight turns. - tui_gateway/prompt_turn.py: forward main's display metadata and cooperative worker retirement while preserving the PR's profile/session incarnation and routing fences. - tui_gateway/session_auto_continue.py: retain retirement-aware continuation dispatch and preserve both display metadata and provenance in inflight snapshots. - tui_gateway/session_history.py: classify provenance through history projection while copying metadata and retaining the original user input for inflight/error recovery. - apps/shared/src/gateway-contract.generated.ts: regenerate from the combined Python contracts. - apps/shared/src/gateway-contract.openrpc.json: regenerate from the combined Python contracts. Extend regressions for shared-tree ignore pruning, synthetic notice reconciliation, metadata/provenance retention and wire validation. Seed valid named-profile identities in MCP, cron and plugin RPC fixtures instead of weakening profile lifecycle guards.
Merge origin/main at 0a8d4ca into feat/desktop-web without rebasing or rewriting the PR history. Resolve hermes_state.py additively: retain profile-incarnation fencing and main's duplicate-holder creation-site diagnostics before registration. Resolve tui_gateway/session_lifecycle.py additively: retain bounded turn and agent-build thread joins and their settlement result, while preserving main's isolated-turn lease deferral before session teardown. Adapt the browser preload bridge to main's required windowControls contract: advertise custom=false and explicitly reject native operations. Add a regression proving the operations cannot close the browser or send requests. Verification: - Conflict-focused Python: 98 passed, 5 skipped across six files. - PR-scoped Python: 2177 passed, 39 skipped, one failure; that database lifecycle failure also reproduces on the exact merged main. - Desktop tests: 981 passed, one skipped across 57 files. - Shared channel tests: 12 passed; TUI gateway client tests: 20 passed. - Desktop/shared typechecks, Webapp build, scoped ESLint, Ruff and conflict-marker/diff checks passed. - Independent read-only merge review found no actionable regressions. Full-suite failures are being compared with the exact merged base; these results are not a claim of a green full suite.
What does this PR do?
Adds
hermes webapp, an authenticated browser-hosted mode for the actual Hermes Desktop renderer. This is not the Web Dashboard: it serves the chat-first Desktop workspace and installs a browser implementation ofwindow.hermesDesktopbacked by the host running Hermes.The implementation keeps the existing authority boundaries instead of cloning Desktop UI or adding a parallel backend:
apps/desktop/dist-webappfrom the pinned repository workspace;Lineage and overlap
Browser bridge/build lineage is carried forward from @adybag14-cyber's feat(termux): add browser-hosted Desktop via existing TUI PTY #85604; the arbitrary-shell design that originally blocked that PR is not reused.
The explicit
hermes webappproduct boundary is carried forward from @seagpt's Add Hermes Webapp command and architecture docs #61171.Related browser/Desktop UX work was reviewed in fix(web): harden desktop webapp chat and mobile controls #60829.
Current main's connection-owner Bot routing (feat(desktop): remote bots open their own Bot Chat without re-homing Desktop (salvage #90006) #92731, salvaging feat(desktop): route remote bot actions by connection #90006) remains authoritative; this PR does not add a second connection resolver.
Rebased onto main's whole-codebase decomposition (refactor: whole-codebase simplification — −34% source LOC, every god file decomposed, zero behavior change #102117,
d3630f853239e8c41ce7201e09fbdf39bcbc5431). Webapp behavior follows the topical modules introduced by main, with host-terminal and profile-lifecycle policy in focused owners. All 28 original commits retain their author metadata and full messages, including contributor trailers. The earlier Shard hermes_cli/web_server.py (god-file decomposition) #78628/Shard tui_gateway/server.py (god-file decomposition) #78630 proposals and cited shards were reviewed before that main update; no code from unmerged PRs was copied.Both source contributors are preserved with
Co-authored-bytrailers.Related Issue
No single issue. Related work: #85604, #61171, and #60829. This PR does not close those threads.
Type of Change
Changes Made
hermes_cli/webapp.py, CLI parser/lifecycle code, and docs: addhermes webapp, isolated renderer builds, process identity,--status/--stop, and loopback-by-default launch behavior.apps/desktop/src/lib/browser-desktop-bridge*.ts: implement the browser preload contract for authenticated REST/gateway calls, uploads, files, Git, previews, clipboard, microphone, profiles, and Bot registry routes.hermes_cli/web_server_chat.py,hermes_cli/web_host_terminal.py, and thechat_ws,files, anduploadsrouters: add authenticated Webapp routing, profile-scoped upload staging, and host-terminal setup through the topical owners. The shared/api/ptytransport remains in the chat router.SessionDBpathname binding/schema initialization, and attachment writes so delete/recreate cannot cross a checked generation boundary. Partial-delete retries durably recover the token. Publication and retirement coordination lives inhermes_cli/profile_lifecycle.py; gateway retirement/fence policy lives intui_gateway/profile_lifecycle.py.How to Test
/api/profiles, Bot roster/routes, and the profile filesystem.Verification
Webapp media validation at
9535f46796372c40e4d7feed83f95e2857a6084e:/hermes/proxies with a named profile. Downloads matched source bytes, including a 17 MiB file and the unsupported-video fallback. The reported H.264/AAC clip, padded with a valid MP4 free box to 20 MiB, played and sought to 35 seconds; audio seeking and HTTP Range bytes also passed. Missing-file errors retained the chat; invalid credentials, cookie-gate bypass attempts, and sensitive-file requests were rejected. All state was disposable and no model inference was performed.Webapp link-default validation at
c791c058f2d981b8404634ab3197c110f36ef615:X-Frame-Optionsand CSPframe-ancestorsrestrictions. Each opened successfully in a separate tab, with no opener or referrer and no preview pane. No model inference was performed.Preview recovery validation at
8b59b8b62200ff0e973abebee2be3f18b0b4026b:hermes webappinstance verified pages denied byX-Frame-Optionsand CSPframe-ancestors, a redirect to a denied page, and a page that permits embedding. The original URL opens only on click, loads successfully in a separate tab, and has no opener or referrer; the preview sandbox remains intact.Local Linux validation at
d08d103d5b813a775f6bd1bed89f02db8c86203c, rebased ontod3630f853239e8c41ce7201e09fbdf39bcbc5431(#102117):scripts/run_tests.sh -j 8 --file-retries 0 --file-timeout 240with-q --tb=short. This covers profile deletion/recreation, live resume, clipboard lifecycle, browser uploads, web-server and gateway regressions, the RPC protocol, and attachments during deferred agent builds. Upload error coverage checks both permission denial (403) and generic I/O failure (500), including cleanup failures.npm test -- --maxWorkers=4 src/app/session/hooks/use-session-actions/utils.test.ts src/app/session/hooks/use-session-actions/provenance.test.ts src/app/session/hooks/use-session-actions.test.tsx.npm run typecheck,npm run lint(zero errors; 134 warnings), andnpm run build:webapppassed.npm run smoke:browser-hostpassed against a realhermes webapp --skip-build --isolated --no-open --port 0process with disposable home/cache state. All four viewports passed. The checks included mobile browser controls, configured-layout persistence, sandboxed HTML previews, and a real host PTY (HERMES_BROWSER_REQUIRE_TERMINAL=1).scripts/check_compat_pointers.py(2,091 pointers), andgit diff --checkpassed.Broader regression validation at the preceding commit
af5014ae2ea9fcfba314556c5e73ca9e0167f840passed 1,695 Python tests with 34 skips across 58 files and 445 Desktop tests with one skip across all 15 changed test files. Ruff passed over all 94 Python files changed by the PR. The Windows checker found zero introduced findings; its 92 reports were on unchanged test lines.The full Desktop suite at that preceding commit completed 9,377 passing tests and seven skips, but exited with one unhandled Radix/jsdom focus-scope teardown exception in the unchanged
project-dialog.test.tsx. A control run on exact maind3630f8532reproduced the same exception type and stack in two other unchanged dialog tests (9,277 passed, six skipped). That inherited full-suite issue remains; the focused suites above pass. Native Windows/macOS validation remains with CI.Security, compatibility, and non-goals
Checklist
Code
fix(scope):,feat(scope):, etc.)scripts/run_tests.shsuite and all tests pass (focused validation is listed above)Documentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — N/A (no new config key)CONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/AScreenshots / Logs
Nous dark video
hermes-webapp-PR-93508-dark.mp4
Download the video (MP4, 40 seconds)
Captured from the actual browser-hosted Desktop renderer at
8b5da64ab107f90d88185ce01e61ee41bbae292c, using the built-in Nous dark theme and disposableHERMES_HOME,HOME, and XDG roots.This is an offline UI walkthrough with a seeded sample conversation, not live model inference. The capture exercised theme switching, real files/Git, the host terminal, sandboxed preview, and refresh persistence. Test counts shown in the video are PR-reported, not independently rerun for the trailer.
Music: an original electronic instrumental synthesized for this trailer; no sampled commercial recording. Capture scope, limitations, and soundtrack provenance.