Skip to content

feat(bot-mode): add scoped cross-gateway Group Chat transport - #97797

Closed
dokterdok wants to merge 3 commits into
NousResearch:mainfrom
dokterdok:feat/bot-mode-cross-gateway-roomlink-20260829
Closed

dokterdok wants to merge 3 commits into
NousResearch:mainfrom
dokterdok:feat/bot-mode-cross-gateway-roomlink-20260829

Conversation

@dokterdok

@dokterdok dokterdok commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

The user problem

#97744 lets a Group Chat continue on one gateway after Desktop closes. A Group
Chat containing Bots from two gateways still needs a safe way for the home
gateway to dispatch an exact member turn, recover it after response loss, and
observe Stop or completion without using Desktop as a courier.

This PR adds the scoped cross-gateway Group Chat transport, internally called RoomLink. It is the direct, text-first layer under the later Desktop continuity UX.

Important

This PR is stacked on #97712 and #97744. Review only the commits after the
current #97744 head ee2f98efe1. It does not enable a new Desktop creation path by itself.

Cross-gateway transport contract

  • Durable /v1/runs admission and status using one idempotency identity and
    immutable request fingerprint.
  • Asynchronous peer commands that recover the same admitted run after response
    loss or process restart.
  • A target-issued RoomLink grant scoped to the exact room, home and authority
    epoch, member, target install, target profile, permissions, execution policy,
    and expiry.
  • Room grant authentication happens before request-body parsing or active-work
    reservation. A status-only grant cannot mint new dispatch authority.
  • Remote execution requires manual or smart approvals; an execution-policy or
    capability change pauses the route for explicit reauthorization.
  • Live capability verification before first dispatch.
  • Direct authenticated transport with credential-safe redirect handling.
  • Durable route and run receipts, bounded status polling, replay-safe dispatch,
    and exact-run Stop.
  • Fail-closed mixed-version behavior: unsupported peers stay unavailable rather
    than weakening the grant or silently running a different Bot.

Reliability behavior

  • A peer proven unreachable before admission keeps the exact turn queued for a
    bounded retry.
  • Timeout, reset, or 5xx after submission remains uncertain and reuses the same
    durable dispatch identity; it is never sent through a second route.
  • Completion may win a Stop race, but Stop remains pending until the exact
    execution generation is terminal.
  • Home restart reloads the route and receipt without resubmitting speculative
    work.
  • Message identity is bound to immutable content; conflicting replay fails.
  • Superseded authority, stale reservations, revoked grants, and late receipts
    cannot commit new room state.

Deliberate boundaries

  • Direct link only; no mesh, relay, or automatic authority failover.
  • Desktop connected to both gateways is not a data path. If only one gateway
    is reachable from every participant, that gateway should hold authority;
    reverse-dial or a relay is a later NAT-traversal increment.
  • Cross-gateway text turns only.
  • No Desktop green path, files, Bot artifacts, or messaging commands.
  • Non-loopback peers require authenticated HTTPS.
  • Application-layer E2EE remains a later transport choice; this PR does not
    introduce a new crypto system.

Those capabilities remain separate layers in #97681.

Source composition

The current squashed head carries the final behavior but does not preserve the
source authors in Git history. The maintainer salvage should use the archived
source-authored train at
dokterdok:archive/bot-mode-roomlink-source-authorship-20260831:

The later commits through b06ea70847 contain the David-authored extraction,
RoomLink integration, and P1/P2 hardening. This preserves the actual authors
without assigning the replacement server implementation to the wrong source.

Validation

Current head 5089c1910b, stacked directly on #97744 head ee2f98efe1:

Check Result
Current-head RoomLink, driver, prompt-fence, service, policy, and authority selection 161 passed
Broader changed-file selection 672 passed / 1 Windows-only skip
Headless in-process two-endpoint HTTP protocol smoke test passed
Prior real text UAT with the home and peer on one private network, then on separate networks passed; the complete integration tree has repeated the current transport over TLS
Disband-prune identity, stale-traffic, and typed expired-history regressions passed in the parent and Layer 3 gates
Ruff on every changed Python file passed
No Desktop, files/artifacts, messaging, or relay path in this diff passed
Fresh exact-head GitHub CI required checks green at 5089c1910b
git diff --check passed

The final branch preserves the source contributor commits and adds only scoped composition, hardening, reconciliation, and hermetic tests. The current head authenticates before reserving work, bounds peer reads by bytes and wall clock, fails closed on policy drift and revoked grants, uses structured revocation errors, and preserves exact policy numbers across JavaScript.

The standalone head has green required CI and 161 focused / 672 broader tests. The complete integration tree in #98307 has repeated same-network TLS text/history, restart, Stop, route revocation, and offline recovery on real gateways without changing this transport boundary.

Related work

Type of change

  • Bug fix
  • New feature
  • Security boundary
  • Tests
  • Refactor

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery comp/tui Terminal UI (ui-tui/ + tui_gateway/) comp/tools Tool registry, model_tools, toolsets needs-decision Awaiting maintainer decision before any implementation sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades area/profiles Multi-profile isolation, HERMES_HOME scoping labels Aug 29, 2026
@dokterdok
dokterdok force-pushed the feat/bot-mode-cross-gateway-roomlink-20260829 branch from 65610d2 to ee6f619 Compare August 29, 2026 09:27
@dokterdok
dokterdok marked this pull request as ready for review August 29, 2026 13:32
@dokterdok
dokterdok force-pushed the feat/bot-mode-cross-gateway-roomlink-20260829 branch from b06ea70 to 51d921b Compare August 29, 2026 23:39
@dokterdok
dokterdok force-pushed the feat/bot-mode-cross-gateway-roomlink-20260829 branch 3 times, most recently from 8b1f913 to bb93d53 Compare August 30, 2026 14:46
@dokterdok
dokterdok force-pushed the feat/bot-mode-cross-gateway-roomlink-20260829 branch from bb93d53 to 1c6bf4c Compare August 30, 2026 14:58
@dokterdok dokterdok changed the title feat(bot-mode): add scoped cross-gateway RoomLink feat(bot-mode): add scoped cross-gateway Group Chat transport Aug 30, 2026

@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 the Layer 3 delta at exact head 1c6bf4cc1a5294d7387bb5dd9eb020dedcef1b44, after #97744 head 9560439e970ca390d1aab4feb7eb649b754da10e. Exact-head CI, Docker, and Nix are green.

P1 security — near-expiry renewal silently authorizes policy and capability drift

The PR contract says execution-policy or capability changes pause the route for explicit reauthorization. The renewal path currently does the opposite:

  • gateway/platforms/api_server_room_grants.py::_handle_room_member_grant_refresh() authenticates the still-live old dispatch grant, recomputes the target's current execution policy, and mints a replacement grant with that new policy digest. It never compares the new digest with the digest authorized by the old grant.
  • tui_gateway/hosted_room_peer_http.py::refresh_grant() then probes the live capability endpoint with the replacement and returns the new catalog.
  • tui_gateway/hosted_room_service.py::_RouteStatusPeerClient persists that replacement and rewrites the pending HostedMemberDispatch to the refreshed catalog and execution-policy digests before dispatch.

Reproduction shape: issue a grant/catalog under policy A; change the target policy or advertised capability catalog to B; let the grant enter the automatic refresh window; dispatch the next exact task. The home gateway renews, adopts B, rewrites the frozen dispatch digests, and executes without the explicit reauthorization this PR promises.

Required repair:

  • renewal must preserve the previously authorized policy/catalog, or return a typed room_reauthorization_required result when either digest changed;
  • the home side must never patch a pending dispatch to a newly observed digest. Any drift must mark the route needs_reauthorization and leave the original work frozen;
  • add near-expiry tests for policy-only drift, capability-only drift, simultaneous drift, unchanged renewal, restart after drift, and response-lost renewal replay.

Additional landing blockers

  • The inherited #97712 replay page still counts Unicode characters instead of UTF-8 bytes.
  • This delta modifies gateway/hosted_rooms.py and tui_gateway/server.py after they exceed the repository's hard 2,000-physical-line ceiling. Those touched seams must be sharded.

This head is not merge-ready. A repaired head needs fresh exact-head proof before downstream Desktop, Files, or Messaging layers consume it.

@dokterdok
dokterdok force-pushed the feat/bot-mode-cross-gateway-roomlink-20260829 branch from 1c6bf4c to 2d7a346 Compare August 30, 2026 20:31

@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 the rewritten Layer 3 carrier at exact head 5089c1910bd50aab508b2b68228d90e8b1d4c5b0 and rechecked the prior near-expiry reauthorization blocker through the target grant handler, peer HTTP client, route-status wrapper, and current regressions.

The prior P1 is repaired on this object. Grant refresh now rejects target execution-policy drift against the previously authorized execution_policy_digest; the home-side refresh passes the frozen capability/policy digests, compares any refreshed catalog to those exact digests, marks the route needs_reauthorization, and refuses dispatch instead of rewriting the pending dispatch to newly observed authority. The regression matrix covers policy-only drift, capability-only drift, and simultaneous drift and proves no refreshed route is published and no peer dispatch occurs. The unchanged path persists the replacement grant before remote admission.

The current three-commit train has exact hosted proof on every surviving object:

  • f5908c18d86a4d4a572a464be2d00e9eb8cec3ee: CI 33332784224, Docker 33332783891, Nix 33332783919 — success.
  • ee2f98efe1fb9f0f5f9a8e98be5d78b962217654: CI 33336745613, Docker 33336745271, Nix 33336745272 — success.
  • 5089c1910bd50aab508b2b68228d90e8b1d4c5b0: CI 33336746960, Docker 33336746521, Nix 33336746522 — success.

I do not see the prior Layer 3 policy/capability-drift defect surviving on 5089c191....

This PR is still not landing-ready for two upstream/structural reasons that exact-head green does not erase. First, its current history is f5908c18... → ee2f98ef... → 5089c191..., while current Layer 1 #97712 has since moved to ad949774f0072499c56c8e0e86fb056cb877b315 on top of f5908c18...; Layer 3 therefore is not a cumulative descendant of the current Layer 1 carrier and cannot transfer current Layer 1 proof forward until the stack is restacked. Second, this cumulative diff still inherits the #97712 modification to hermes_cli/web_server.py, which remains above the repository's 2,000-physical-line touched-file ceiling. Current live main@d63f996a757f6255fc1454239616ab4b4435e0f5 is also two path-disjoint Photon/docs commits beyond the submitted base, so the final accepted stack still needs current-main recomposition.

Required closure: finish the Layer 1 structural extraction, restack Layers 2–3 on the final accepted #97712 object without losing this fail-closed reauthorization repair, then reacquire CI/Docker/Nix for every surviving commit in the rebuilt train. On the current 5089c191... Layer 3 delta itself, I do not see another source-level authorization blocker.

@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.

Landing-edge correction to my immediately preceding review: main moved while that review was being read back, and Layer 1 has now been salvaged directly into upstream.

Current main is ed3562bbbcd8a5323be96b81812291faae99e010. #97712 is now closed/unmerged, but its accepted foundation landed as cbc67b939f71389494b842b5499ebe104ebda46e followed directly by maintainer follow-up cc4b5ba1fc48e86df99f139a46232fd5debf1834, which carries replay-page authority lineage and the sabotage-sensitive UTF-8 byte-bound proof.

That supersedes two landing statements in review 5062754503:

  1. Layer 3 no longer needs to wait for #97712 itself to become an accepted current-main carrier; main now owns the Layer 1 foundation.
  2. The inherited hermes_cli/web_server.py >2K delta is not a downstream diff blocker once this stack is correctly rebased and the already-landed Layer 1 ancestor is dropped from the PR history.

The Layer 3 source verdict is unchanged: the prior policy/capability auto-reauthorization P1 is repaired on 5089c191..., and the three currently surviving branch commits had exact-green CI/Docker/Nix when checked.

The required closure is now a real restack onto salvaged main, not a wait on #97712: rebuild #97744 on main, then rebuild this Layer 3 commit on that Layer 2 object, preserving the fail-closed reauthorization repair. Do not replay f5908c18... as a new duplicate ancestor. During the rebase, preserve current-main compression changes in gateway/run.py; main modified that seam after this stack was cut. Reacquire CI/Docker/Nix on every surviving rebuilt commit before transferring any prior green receipt.

This correction is about the moving landing edge only; it does not retract the current-head source review.

@dokterdok

Copy link
Copy Markdown
Contributor Author

Maintainer-salvage note: the current squashed Layer-3 head does not preserve the source authors, but the source-authored carrier train is still available on dokterdok:archive/bot-mode-roomlink-source-authorship-20260831 at b06ea70847.

The commits to preserve before the David-authored RoomLink reconciliation are:

The later commits through b06ea70847 are the David-authored extraction, scoped RoomLink integration, and current hardening. The two odd source emails are already mapped through contributors/emails/ in that train.

For Layer 4, the #96162 behavior is separately available as a current-main TSX forward-port authored by jugol at dokterdok:archive/bot-mode-jugol-forward-port-20260831, commit fe788f2162 (20 focused UI tests, TypeScript, and attribution audit pass). This avoids assigning jugol's work to the broader David continuity commit while retaining #96726's typed architecture.

I will not rewrite the public heads while the maintainer sweep is active; these archive refs exist only so the salvage can cherry-pick the actual authors.

@dokterdok

dokterdok commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

@teknium1 — Current-main salvage evidence is now available in draft #99159. Its first commit 3f72184713 replays this Layer-3 behavior onto merged #99007/#99099, drops the obsolete Layer-1/2 ancestors, and preserves #99099 bounded cancel-race routing together with exact peer Stop/settlement. The cumulative current-main branch is mergeable and every required CI/Docker/Nix gate is green.

For the actual Layer-3 merge carrier, preserve the source-authored train from dokterdok:archive/bot-mode-roomlink-source-authorship-20260831 (b06ea70847) rather than taking the David-squashed evidence commit. #99107 remains the safety prerequisite; automatic authority takeover stays disabled.

teknium1 added a commit that referenced this pull request Aug 31, 2026
… drift

Follow-ups on the salvaged #97797 transport:

- Blocker 2 from the #97681 exact-head review claimed near-expiry refresh
  silently mints against the target's CURRENT policy. On this head the
  handler DOES refuse drift (_require_unchanged_execution_policy -> 403
  room_reauthorization_required), but nothing pinned the handler-level
  behavior: removing the drift check still passed the entire grants suite
  (the check was only unit-tested in isolation). New HTTP-level regression
  test drives /v1/room-members/grants/refresh with a drifted-policy grant
  and requires the 403; sabotage-verified (check removed -> test fails).
- cancel() conflict resolution: keeps our race-retry routing loop from
  #99099 with this layer's peer-stop acknowledgement body inside it
  (peer receipt -> settle completion -> local interrupt escalation).
- docs: NAT one-way-reachability note in bot-mode.md — Desktop is a viewer,
  not a relay; put room authority on the host everyone can reach (field
  finding from /bin/bash on #97681).
teknium1 added a commit that referenced this pull request Aug 31, 2026
… drift

Follow-ups on the salvaged #97797 transport:

- Blocker 2 from the #97681 exact-head review claimed near-expiry refresh
  silently mints against the target's CURRENT policy. On this head the
  handler DOES refuse drift (_require_unchanged_execution_policy -> 403
  room_reauthorization_required), but nothing pinned the handler-level
  behavior: removing the drift check still passed the entire grants suite
  (the check was only unit-tested in isolation). New HTTP-level regression
  test drives /v1/room-members/grants/refresh with a drifted-policy grant
  and requires the 403; sabotage-verified (check removed -> test fails).
- cancel() conflict resolution: keeps our race-retry routing loop from
  #99099 with this layer's peer-stop acknowledgement body inside it
  (peer receipt -> settle completion -> local interrupt escalation).
- docs: NAT one-way-reachability note in bot-mode.md — Desktop is a viewer,
  not a relay; put room authority on the host everyone can reach (field
  finding from /bin/bash on #97681).
@teknium1

Copy link
Copy Markdown
Collaborator

Merged via #99244 (rebase-merge, your commit and authorship preserved) — #99244, on main at 1cf3639.

Follow-ups that rode on top:

  • Blocker 2 from @andrexibiza's exact-head review is confirmed FIXED on your current head — the refresh handler refuses execution-policy drift with 403 room_reauthorization_required. What was missing was pinning: removing _require_unchanged_execution_policy from the handler passed the entire grants suite (the check was only unit-tested in isolation). Added an HTTP-level regression test that drives a drifted-policy grant through /v1/room-members/grants/refresh and requires the 403; sabotage-verified.
  • cancel() now combines the feat(bot-mode): same-gateway Group Chats keep running after Desktop closes (salvage #97744) #99099 race-retry routing loop with this layer's peer-stop acknowledgement escalation.
  • Docs: NAT one-way-reachability note in bot-mode.md (@/bin/bash's local↔VPS field finding) — Desktop is a viewer, not a relay; put room authority on the host every participant can reach.

Also heads-up: your remaining branches now rebase onto the replication/takeover layer from #99047 (groups.replicate/replica_state/promote/demote, authority stamps on replay pages). #97846 is next in the review order — we'll fix the 4123-terminal classification in that salvage. #98072 should reuse the authority_gateway_id/authority_epoch lineage columns for attachment batches.

@teknium1 teknium1 closed this Aug 31, 2026
EduardoSolanas pushed a commit to EduardoSolanas/hermes-agent that referenced this pull request Sep 2, 2026
… drift

Follow-ups on the salvaged NousResearch#97797 transport:

- Blocker 2 from the NousResearch#97681 exact-head review claimed near-expiry refresh
  silently mints against the target's CURRENT policy. On this head the
  handler DOES refuse drift (_require_unchanged_execution_policy -> 403
  room_reauthorization_required), but nothing pinned the handler-level
  behavior: removing the drift check still passed the entire grants suite
  (the check was only unit-tested in isolation). New HTTP-level regression
  test drives /v1/room-members/grants/refresh with a drifted-policy grant
  and requires the 403; sabotage-verified (check removed -> test fails).
- cancel() conflict resolution: keeps our race-retry routing loop from
  NousResearch#99099 with this layer's peer-stop acknowledgement body inside it
  (peer receipt -> settle completion -> local interrupt escalation).
- docs: NAT one-way-reachability note in bot-mode.md — Desktop is a viewer,
  not a relay; put room authority on the host everyone can reach (field
  finding from /bin/bash on NousResearch#97681).
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
… drift

Follow-ups on the salvaged NousResearch#97797 transport:

- Blocker 2 from the NousResearch#97681 exact-head review claimed near-expiry refresh
  silently mints against the target's CURRENT policy. On this head the
  handler DOES refuse drift (_require_unchanged_execution_policy -> 403
  room_reauthorization_required), but nothing pinned the handler-level
  behavior: removing the drift check still passed the entire grants suite
  (the check was only unit-tested in isolation). New HTTP-level regression
  test drives /v1/room-members/grants/refresh with a drifted-policy grant
  and requires the 403; sabotage-verified (check removed -> test fails).
- cancel() conflict resolution: keeps our race-retry routing loop from
  NousResearch#99099 with this layer's peer-stop acknowledgement body inside it
  (peer receipt -> settle completion -> local interrupt escalation).
- docs: NAT one-way-reachability note in bot-mode.md — Desktop is a viewer,
  not a relay; put room authority on the host everyone can reach (field
  finding from /bin/bash on NousResearch#97681).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/profiles Multi-profile isolation, HERMES_HOME scoping comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery comp/tools Tool registry, model_tools, toolsets comp/tui Terminal UI (ui-tui/ + tui_gateway/) needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages 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.

4 participants