Skip to content

feat(webapp): serve Desktop renderer in browsers - #93508

Open
BearHuddleston wants to merge 76 commits into
NousResearch:mainfrom
BearHuddleston:feat/desktop-web
Open

BearHuddleston wants to merge 76 commits into
NousResearch:mainfrom
BearHuddleston:feat/desktop-web

Conversation

@BearHuddleston

@BearHuddleston BearHuddleston commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

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 of window.hermesDesktop backed by the host running Hermes.

The implementation keeps the existing authority boundaries instead of cloning Desktop UI or adding a parallel backend:

  • the renderer is built separately to apps/desktop/dist-webapp from the pinned repository workspace;
  • the existing FastAPI authentication/session-token and one-time WebSocket-ticket controls gate the surface;
  • host files, Git, previews, uploads, profiles, terminal, gateway, and Bot routing reuse existing profile-scoped APIs;
  • native-only Electron capabilities are absent or fail explicitly rather than being falsely emulated;
  • non-loopback binds remain fail-closed behind configured authentication.

Lineage and overlap

Both source contributors are preserved with Co-authored-by trailers.

Related Issue

No single issue. Related work: #85604, #61171, and #60829. This PR does not close those threads.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • hermes_cli/webapp.py, CLI parser/lifecycle code, and docs: add hermes 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 the chat_ws, files, and uploads routers: add authenticated Webapp routing, profile-scoped upload staging, and host-terminal setup through the topical owners. The shared /api/pty transport remains in the chat router.
  • Desktop preview/files/layout/terminal surfaces: website links open in a separate browser tab by default in Webapp; Electron retains its embedded browser with local or remote gateways. Explicit URL previews retain the sandboxed iframe and Open in browser recovery action. Other browser adaptations include deliberate native-only degradation, configured-layout persistence, and refresh-safe terminal history.
  • Webapp downloads and media: implement the browser save bridge using authenticated download URLs and a HEAD check before handing off to the browser. Audio/video use authenticated HTTP streams with Range support. URLs retain the active profile and proxy base path; Electron keeps its native save dialog and media protocol. Existing managed-file access rules and size limits still apply.
  • Profile lifecycle: persist a fresh incarnation per create/import, preserve it across rename, carry it through sessions/workers, and use the same cross-process lifecycle lease for legacy marker backfill, SessionDB pathname 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 in hermes_cli/profile_lifecycle.py; gateway retirement/fence policy lives in tui_gateway/profile_lifecycle.py.
  • Upload and attachment handling: reuse the incoming upload spool, check its actual length, and carry captured installation/profile/incarnation ownership through browser picks, drops, edits, drafts, and retries. Already-staged files attach through the owning backend and retain the copy into the container-visible attachment cache.
  • Clipboard publication: extract native clipboard images outside lifecycle/session locks, then validate the captured session and profile generation before publishing the image.
  • Session display and recovery: derive human/runtime provenance from the backend's canonical classifier and carry it through REST/RPC hydration and reconciliation. Runtime journal boundaries preserve partial replies, tool/reasoning structure, human corrections, and stream ownership across reloads and reused stream IDs; completed replies retire the journal tail. Older gateways retain the existing fallback, with durable row IDs compared before text normalization.
  • Detached turn continuity: a brief browser disconnect retains the running session and worker for a bounded reconnect grace period. Resume, activation, and prompt submission check profile generation and disconnect ownership before reattaching.
  • Shared policy: use one typed Git REST mapping, the existing upload-generation resolver and desktop control-plane process classifier, an explicit staging destination for private profile initialization, and shared attachment response/error helpers. Profile checks access their lifecycle owner directly.
  • Tests and smoke coverage: add browser bridge, uploads, Webapp command/lifecycle, same-name recreation ABA, deterministic barriers at attachment write/DB connect/legacy backfill, fail-closed lease-timeout behavior, concurrent cross-process marker publication, named-launch profiles, partial-delete retry, generation-scoped resume, fallback enumeration, real Chromium host-terminal/layout checks, concurrent clipboard extraction, staged-file ownership, canonical display provenance, and runtime journal recovery.

How to Test

  1. Build the browser renderer:
    hermes webapp --build-only --force-build
  2. Start locally:
    hermes webapp --host 127.0.0.1 --port 9119
  3. Verify Desktop sessions/files/Git/previews/terminal, create and delete a profile, refresh, and confirm the deleted profile remains absent from /api/profiles, Bot roster/routes, and the profile filesystem.
  4. Click a website link in Webapp: it should open in a separate browser tab without creating a preview pane. In Electron, a normal click should still open its embedded browser, including when connected to a remote gateway.
  5. Explicitly choose Open in app from a link context menu to use an embedded preview. Sites that reject embedding should retain the Open in browser recovery action.
  6. Download a file from a chat attachment, play and seek an inline video/audio attachment, and try Open video file for an unsupported format. Missing files should show a download error while keeping the chat open.
  7. For remote use, configure dashboard authentication first, then launch with an explicit non-loopback bind.

Verification

Webapp media validation at 9535f46796372c40e4d7feed83f95e2857a6084e:

  • 103 focused Desktop tests and 77 Python tests passed, covering browser and native media, file downloads, auth, and proxy prefixes. The new browser regressions and HTTP endpoint assertions failed before the fix. The auth-gate file passed in an isolated network namespace because this host already has a service on port 9119. Typecheck, lint (zero errors; 134 existing warnings), Ruff, and the browser build passed.
  • Real Chromium exercised both loopback token auth and a real password-login session cookie through /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.
  • Browser smoke passed all four viewports, layout persistence, sandboxed HTML, and a real host PTY. The refreshed test instance also served the original reported MP4 successfully through its stream and download endpoints.

Webapp link-default validation at c791c058f2d981b8404634ab3197c110f36ef615:

  • 98 focused Desktop tests passed across four files, covering Webapp click-gesture handoff, Electron embedded defaults with local and remote gateways, filesystem routing, preview behavior, and context-menu actions. The new Webapp regression failed before the change. Typecheck, lint (zero errors; 134 existing warnings), and the browser build passed.
  • Real Chromium against a disposable Webapp clicked normal links from a seeded stored conversation to pages with X-Frame-Options and CSP frame-ancestors restrictions. Each opened successfully in a separate tab, with no opener or referrer and no preview pane. No model inference was performed.
  • Browser smoke passed all four viewports, layout persistence, sandboxed HTML, and a real host PTY.

Preview recovery validation at 8b59b8b62200ff0e973abebee2be3f18b0b4026b:

  • 78 focused Desktop tests passed across four files, including the regression that failed before the fix because embedded previews had no external-opening action. Typecheck, lint (zero errors; only existing warnings remain in the touched files), and the browser renderer build passed.
  • Real Chromium against a disposable hermes webapp instance verified pages denied by X-Frame-Options and CSP frame-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.
  • The existing browser smoke passed all four viewports, layout persistence, sandboxed HTML, and a real host PTY. The browser fixture skips model onboarding and performs no model inference.

Local Linux validation at d08d103d5b813a775f6bd1bed89f02db8c86203c, rebased onto d3630f853239e8c41ce7201e09fbdf39bcbc5431 (#102117):

  • 978 Python tests passed, 11 skipped across eight files through scripts/run_tests.sh -j 8 --file-retries 0 --file-timeout 240 with -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.
  • 234 Desktop tests passed, one skipped across three files with 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), and npm run build:webapp passed.
  • npm run smoke:browser-host passed against a real hermes webapp --skip-build --isolated --no-open --port 0 process 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).
  • Ruff over all six Python files touched by the cleanup, scripts/check_compat_pointers.py (2,091 pointers), and git diff --check passed.

Broader regression validation at the preceding commit af5014ae2ea9fcfba314556c5e73ca9e0167f840 passed 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 main d3630f8532 reproduced 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

  • An authenticated Webapp user has host-level Hermes capabilities, including an interactive shell as the account running Hermes. The docs call this out explicitly; trusted VPN/SSH-tunnel access is preferred and direct internet exposure should use OAuth/OIDC plus HTTPS.
  • Browser-selected files are uploaded as bytes into profile-scoped host staging; client filesystem paths are never treated as host paths.
  • URL previews use a capability-minimal sandboxed iframe. Electron DevTools/trusted-input automation, native window inspection, global shortcuts, always-on-top/HUD behavior, external-terminal launch, and native updating remain Electron-only.
  • No direct or transitive dependency versions changed.
  • This does not redesign the Web Dashboard, replace native Desktop, add an unauthenticated shell transport, or claim arbitrary access to a browser client's filesystem.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run the full scripts/run_tests.sh suite and all tests pass (focused validation is listed above)
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: Linux; Windows/macOS-specific lanes are left to CI

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A (no new config key)
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — Linux tested; Windows footgun scan has zero introduced findings; CI provides native lanes
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A

Screenshots / 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 disposable HERMES_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.

@BearHuddleston
BearHuddleston requested a review from a team August 24, 2026 03:46
@alt-glitch alt-glitch added type/feature New feature or request comp/desktop Electron desktop app (apps/desktop/*) comp/cli CLI entry point, hermes_cli/, setup wizard comp/tui Terminal UI (ui-tui/ + tui_gateway/) comp/dashboard Web dashboard / control panel UI (dashboard/, landing) P3 Low — cosmetic, nice to have needs-decision Awaiting maintainer decision before any implementation sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Aug 24, 2026
@BearHuddleston
BearHuddleston force-pushed the feat/desktop-web branch 2 times, most recently from 25fec8b to e1e8c97 Compare August 24, 2026 04:22
@intellectronica

Copy link
Copy Markdown
Contributor

Awesome! Please can this land? I'd really like to use this.

@andrexibiza andrexibiza left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 opening state.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 explicit hermes webapp product-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.py and 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 andrexibiza left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@BearHuddleston
BearHuddleston force-pushed the feat/desktop-web branch 2 times, most recently from 752b001 to 60c5c8a Compare August 26, 2026 22:24
@BearHuddleston

BearHuddleston commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

@andrexibiza Thanks for the detailed review. Both blockers are addressed on exact head fcf7b1ae99427b84c569d4350c83731d65a43180.

1. Same-name profile recreation / ABA

The path is no longer the authority token:

  • every named-profile create/import publishes a fresh persisted 128-bit incarnation; rename preserves it;
  • existing profiles backfill atomically while holding the same cross-process lifecycle lease as create/delete/rename, with real Linux/macOS/Windows test lanes;
  • sessions, deferred builders, compute-host frames, attachment writers, and profile DB openers retain the expected incarnation;
  • SessionDB holds the lifecycle lease through path preflight, connection binding, PRAGMAs, and schema initialization; once bound, its tracked SQLite handle prevents deletion/recreation until close;
  • delete/rename retirement is generation-aware, and the external tombstone durably carries the token so a partial rmtree retry cannot strand the profile;
  • live-session resume/reuse is scoped by profile path and incarnation;
  • attachment mkdir/write/rollback holds the same lifecycle lease, ordered lifecycle → sessions → pathname, so mutation cannot cross into generation B.

The vertical regression performs create(A) → retain A session → delete(A) → recreate(B): A cannot create B's state.db or write B's attachment; B succeeds. Deterministic barriers now pause immediately before attachment write_bytes, SessionDB connect, and legacy marker publication; concurrent delete/recreate must time out on the lifecycle lease and generation B remains untouched. A stale or unstamped compute-host frame is rejected before agent/secret/DB construction.

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 FileNotFoundError path, without rewriting timeout exceptions raised by the protected operation itself.

Primary owners: hermes_cli/profile_incarnation.py, tui_gateway/profile_lifecycle.py, and the optional expected_profile_incarnation boundary on SessionDB.

2. Web/TUI decomposition and contributor credit

I 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 main, and their proposed modules are absent from current main. The parent decomposition issues are also still needs-decision; their own survivor-law text distinguishes provenance from automatic mutation authority.

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:

  • hermes_cli/web_host_terminal.py owns Webapp host-terminal policy, shell resolution, cwd/profile environment, and argv construction; web_server.py retains thin compatibility wrappers and endpoint wiring;
  • tui_gateway/profile_lifecycle.py owns generation fencing and session-retirement orchestration; server.py retains compatibility entry points and call-site wiring;
  • uploads were already owned by hermes_cli/web_routers/uploads.py;
  • the auth delta remains a small extension to the existing auth gate rather than a competing generic shard.

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 15b673d1784; I dropped the duplicate local commit so Trevor Nash-Keller's upstream authorship is preserved.

Verification

  • published against upstream/main@8fdda828a81f98ea28dfe1eeea3a1a4562a2b376; 5 commits ahead, 0 behind at publication, with clean synthetic merge tree b15be5ee9a9f7dd7fcb9660d1bc06a612085289e;
  • canonical affected Python matrix with retries disabled: 1,262 passed, 0 failed, 15 native-OS skips across 17 selected owner files;
  • exact-current-upstream baseline: the one excluded FTS trace-count assertion reproduces as 1 failed, 244 deselected on upstream/main@77001a6be76;
  • canonical workspace runner: 11/11 passed (Desktop lint 140 warnings, 0 errors);
  • Ruff, 1,022-file Windows-footgun scan, uv lock --check, diff/conflict checks: passed;
  • GitHub/local file lists match exactly: 80/80.

Exact-head GitHub CI settled at 24 passed, 11 skipped, 0 failed across CI 33022870375, Docker amd64/arm64 33022868880, and Nix 33022868885. After the green run, main advanced by one formatting-only Desktop commit (77001a6be76) touching no PR-modified file; the branch is now 5 ahead/1 behind and the current-main synthetic merge remains clean at tree 494f8c2372d7b33ee34a3e2056587bd777d5c9d7. Two independent final reviews of the updated locking/connection-tracking diff both returned approve with no findings.

@andrexibiza andrexibiza left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.py gives 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;
  • ProfileLifecycleFence keeps 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;
  • SessionDB checks 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.db or 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.

jquesnelle and others added 30 commits September 9, 2026 15:52
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools area/profiles Multi-profile isolation, HERMES_HOME scoping comp/cli CLI entry point, hermes_cli/, setup wizard comp/dashboard Web dashboard / control panel UI (dashboard/, landing) comp/desktop Electron desktop app (apps/desktop/*) comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants