Skip to content

feat(bot-mode): add scoped cross-gateway RoomLink - #95966

Closed
dokterdok wants to merge 24 commits into
NousResearch:mainfrom
dokterdok:wip/95163-roomlink-backend
Closed

dokterdok wants to merge 24 commits into
NousResearch:mainfrom
dokterdok:wip/95163-roomlink-backend

Conversation

@dokterdok

@dokterdok dokterdok commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Same-gateway rooms can keep working after Desktop closes, but a room containing
Bots from two gateways still needs Desktop to relay every turn.

This stacked draft adds a narrow gateway-to-gateway RoomLink so the home gateway
can dispatch an exact member turn directly, recover it after response loss, and
observe terminal state without a proprietary cloud coordinator.

The transport is text-first, explicit, scoped per room/member/profile, and
fail-closed. No user-facing cross-gateway creation path is enabled in this PR.

When a peer is provably unreachable before admission, the exact turn stays in
the durable room queue and retries with capped backoff. A timeout, connection
reset, or HTTP 5xx remains uncertain instead: it uses the same bounded
idempotent recovery path and never silently fails over to a second route.

Security and identity boundary

  • Target gateway issues the grant; Desktop cannot mint peer authority.
  • Grant binds room, home install, authority gateway and epoch, member, target
    install, target profile, permissions, and expiry.
  • Non-loopback peers require HTTPS. API keys create invitations only; later
    calls use room-scoped grants.
  • Target verifies the live capability digest on first dispatch.
  • Redirects use the credential-safe opener and never forward credentials to a
    different origin.
  • Approval accepts only Allow once or Deny and binds the exact
    request_id, task, generation, member, and grant.
  • A volatile idempotency store disables RoomLink advertisement and invitation.

Reliability boundary

  • Admission uses one deterministic idempotency key and fingerprint.
  • If the acceptance response is lost, the home replays the identical admission
    and recovers the same run and execution generation.
  • Polling observes only the current receipt, shares one status sample, backs off
    from 100 ms to 2 s, and compacts terminal observations.
  • Stop remains stopping until the target run is terminal. Completion may win.
  • Home restart reloads the encrypted route and durable run receipt.
  • Revocation removes the target grant before deleting local route state.
  • Protocol v1 drafts quarantine rather than weakening the unpublished v2 wire.

API-server ownership

This PR closes the decomposition blocker raised on the first review rather than
adding another authority cluster to gateway/platforms/api_server.py:

  • run control: api_server_runs.py (1,388 lines);
  • scoped grant handlers: api_server_room_grants.py (358 lines);
  • durable idempotency: api_server_run_idempotency.py (265 lines);
  • dispatch validation: api_server_room_dispatch.py (146 lines).

Against the peer-run parent, api_server.py shrinks from 8,859 to 7,901 lines
(+141/-1,099). The remaining changes there are imports, stable re-exports,
route wiring, and thin adapter delegates. Reducing the pre-existing 8K file
below 2K would require a separate extraction of unrelated APIs.

Boundaries

Validation

Rebased on verified boundary origin/main@f3cbb262c1, exact backend head
7cf0b44392:

Check Result
Focused RoomLink Python boundary after the final rebase 331 passed
Ruff and py_compile passed
Contributor attribution audit passed
git diff --check passed
Exact-head CI on 7cf0b44392 running after the final rebase

The final UAT also covered named Bot profiles. A named profile now inherits the
gateway root's advertised RoomLink endpoint unless it explicitly overrides it;
models, credentials, tools, and every other profile setting remain isolated.
This keeps adding a Bot on the normal green path instead of requiring repeated
network configuration per profile.

Terminal peer Stop was revalidated after a real two-host canary exposed a
history/status ordering edge. Home now accepts cancellation only for the exact
peer task and execution generation before consulting the lossy history view.

The isolated two-host canary must print:

UAT_OK remote_reply=1 restart_recovered=1 stop_acknowledged=1 target_terminal=1 scoped_route_revoked=1

That canary passed on the complete integration head using isolated gateway
processes, temporary state, a temporary scoped key, and a temporary HTTPS
route. Production Hermes state and processes were not used.

An additional refused-port UAT proved the offline contract: queued work stayed
durable with blocked=0, the route became unavailable, reconnect admitted one
run, published one reply, and returned the route to ready.

Related work

Type of change

  • Bug fix
  • New feature
  • Security fix
  • Documentation update
  • Tests
  • Refactor

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have needs-decision Awaiting maintainer decision before any implementation comp/desktop Electron desktop app (apps/desktop/*) comp/gateway Gateway runner, session dispatch, delivery comp/cli CLI entry point, hermes_cli/, setup wizard comp/tui Terminal UI (ui-tui/ + tui_gateway/) area/profiles Multi-profile isolation, HERMES_HOME scoping 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 labels Aug 27, 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 exact head f751838e621a3e77b22df004eb086f93db0ea75b against live main@7a1aafb4e1dac5d2840cd3ab524f6d6bd0658694. The current object is 13 commits ahead / 12 behind its actual live-main merge base e1e72f109c9540d6ed2aceeaf1b5bfcacbab5efc. There were no existing PR comments/reviews when I started. I read the three RoomLink-specific commits above f9821fb1caf67fa54895fe312cd0896067ebdef3, the exact grant/catalog/dispatch ABI, the HTTP and peer-session transports, Desktop capability planning, relevant tests, source-PR lineage, exact-head CI, and the surrounding hosted-room stack.

There is a lot here worth preserving. The v2 grant expands authority into explicit room/home/authority/member/target/profile coordinates rather than a bearer-shaped ambient permission; HostedMemberDispatch binds task + execution generation + source sequence + prompt/capability digests; non-loopback RoomLink is HTTPS-only; credentialed HTTP goes through the cross-origin header guard; response-loss recovery replays one deterministic admission identity; and Stop stays tied to the exact attempt instead of guessing from current presentation state. The preserved source lineage from #88408 (pesho-vsn), #94336 (giaiant), #88819 (pierrenode), and #93952 (liuhao1024) is also explicit rather than being silently absorbed. Good work on those boundaries. 🚀

I do have two merge blockers on this exact parent object, plus one exact-head acceptance failure.

P1 — the exact backend speaks RoomLink v2 while the exact Desktop contract still accepts only v1

gateway/hosted_room_peer.py says the important part plainly:

# Version 2 adds authority/member lineage to scoped grants. It is intentionally
# not wire-compatible with the unpublished v1 draft; mixed gateways must fall
# back to Desktop-driven rooms instead of accepting a weaker token shape.
PROTOCOL_VERSION = 2

catalog_mapping() / local_catalog_mapping() default protocol_versions to that PROTOCOL_VERSION, so this head's gateway advertises v2. PeerHostedRoomTransport.submit() likewise serializes protocol_version=PROTOCOL_VERSION into the recipient-validated dispatch.

But this same head's apps/desktop/src/plugins/hermes-bots/hosted-room-client.js::resolveAutonomousRoomPlan() requires v1 in both compatibility checks:

roomLink?.catalog?.protocolVersions?.includes(1)
...
!roomLink?.catalog?.protocolVersions?.includes(1)

and hosted-room-client.test.mjs pins the supposed capable home/peer fixtures to protocol_versions: [1]. So the exact v2 catalog produced by this backend is classified as needing setup by the exact client ABI in the same object, while the v1 catalog that the backend explicitly says must be quarantined is the one the planner accepts.

This is already wired code, not dead prose: the Bot plugin classifies groups.capabilities responses and passes them to resolveAutonomousRoomPlan(). The immediate stacked child #95967 actually demonstrates the missing repair: it introduces ROOM_LINK_PROTOCOL_VERSION = 2 and changes both checks to .includes(ROOM_LINK_PROTOCOL_VERSION). That repair belongs in this parent contract; the child should not be required to make its dependency internally consistent.

Required repair: make the RoomLink wire version one explicit shared/parity-tested contract and require v2 at the Desktop compatibility boundary. Update the parent tests so [2] is accepted and [1] is rejected/quarantined. Add one vertical regression that takes the exact current groups.capabilities.room_link.catalog shape from the backend through classifyHostedRoomCapability() + resolveAutonomousRoomPlan() and proves a qualifying two-gateway plan succeeds, while the unpublished v1 catalog does not silently downgrade.

P1 — this stack regrows gateway/platforms/api_server.py after its godfile decomposition contract

The RoomLink-specific delta above f9821fb1… adds 602 lines and removes 28 in gateway/platforms/api_server.py; the complete stacked head is +1,137/-37 there versus live main. The exact submitted file still extends past line 7,000.

That violates the repository's standing decomposition invariant, not a style preference. #78643 is the owning gateway/platforms/api_server.py decomposition issue and states that godfiles are sharded and never regrown; #78647 records api_server.py as one of the completed godfile kills. The already-open shard #83546 owns the idempotency cluster specifically, extracting _IdempotencyCache, _idem_cache, and _make_request_fingerprint into gateway/platforms/api_server_idempotency.py with the old import surface preserved.

That is directly adjacent to the source work this RoomLink stack composes: #88408/#94336 add durable/idempotent /v1/runs behavior, and this branch currently reconciles those semantics by growing the monolith again. The correct composition is to keep the behavior and contributor credit but land it through the accepted sub-2k API-server seams rather than restore authority-bearing behavior to the killed godfile.

Required repair: extract/reconcile the peer-run admission/status/RoomLink API additions into coherent sub-2k modules (and the existing idempotency seam where applicable), preserving stable re-exports and monkeypatch surfaces. The final changed production files need to satisfy the hard 2K gate; do not resolve #83546/#78643 by textual conflict or by abandoning the shard.

Exact-head acceptance is not green

At review time the exact f751838e… workflows are:

  • CI 33029962978failure; the failing job is Python tests / Run tests (98379997648). I do not have a reliable failed-step log receipt here, so I am not inventing a cause.
  • Docker 33029962512 — success.
  • Nix 33029962541 — success.

The isolated two-host canary and 281 focused tests are useful development evidence, but they do not replace the red exact-head CI object. Every commit in the eventual landing stack needs its own truthful acceptance state.

Graph / landing order

The current ownership boundaries are coherent if kept explicit:

  • #95314 remains the authority/replay foundation; #95622 is the same-gateway driver above it.
  • #95965 is a composition carrier, not a silent supersession of #88408/#94336/#88819/#93952. If those source PRs land independently first, drop the patch-identical source commits and retain their authorship/credit.
  • #95966 should own the scoped gateway-to-gateway RoomLink ABI and transport. It needs to be internally v2-consistent before anything consumes it.
  • #95967 is the Desktop/user-facing consumer above this parent. Its v2 constant fix should be pulled down rather than used to mask the parent's ABI mismatch.
  • #92931 remains complementary renderer relay/settlement fallback, not a second gateway authority or queue owner. #91911 remains the broader identity/delivery/approval/cancellation control-plane direction.
  • #78643/#83546 own the API-server decomposition seam that this stack must compose with.

Once protocol parity is repaired at the parent boundary, the API-server work is restored to shard discipline, and the exact head is green, the rest of the scoped-grant/recovery design is shaped well for the cross-gateway layer. The strongest part of this PR is that it is already trying to make every consequential operation consume qualified identity rather than reconstruct it downstream; the remaining work is to apply that same exactness to the wire version and to the repository's module ownership boundary.

@dokterdok
dokterdok force-pushed the wip/95163-roomlink-backend branch 2 times, most recently from f9492d3 to e2686ff Compare August 27, 2026 05:45
@dokterdok

dokterdok commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

@andrexibiza thanks for the exact-head review. Both blockers are addressed on the final rebased backend head d41704b4a9 over main@6defe7eb6c: the Desktop contract accepts only the shared RoomLink v2 constant ([2] accepted, [1] quarantined), and the API work is split into four focused sub-2K modules while api_server.py shrinks by 958 lines versus the peer parent. The bare-fixture teardown and approval-redaction ownership regressions are also fixed. Exact evidence: 591 changed-path Python tests passed (the one local SQLite health failure reproduces unchanged on main), 653 Bot Mode plugin tests passed, typecheck/build/Ruff/attribution/diff checks passed, and all exact-head GitHub checks are green.

@dokterdok
dokterdok force-pushed the wip/95163-roomlink-backend branch 4 times, most recently from e528196 to 3fd47b2 Compare August 27, 2026 11:16
@dokterdok
dokterdok force-pushed the wip/95163-roomlink-backend branch from 3fd47b2 to f13a13f Compare August 27, 2026 16:32
@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference, author can ignore or act on any point.

This diff is too large for an automated line-level review (>30 files or >200KB). Recommend a manual review.

@dokterdok

Copy link
Copy Markdown
Contributor Author

Closing this cumulative prototype now that #97681 captures the user contract, safety boundaries, and phased landing plan against the rebuilt Bot Mode tree. Its useful layer can be re-cut as a narrow current-main contribution after the foundation direction is agreed.

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/desktop Electron desktop app (apps/desktop/*) comp/gateway Gateway runner, session dispatch, delivery 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.

6 participants