Skip to content

fix(connections): every gateway client acts through the room's own connection - #5980

Merged
qingyun-wu merged 6 commits into
mainfrom
feat/gateway-clients-room-resolver
Oct 28, 2026
Merged

qingyun-wu merged 6 commits into
mainfrom
feat/gateway-clients-room-resolver

Conversation

@qingyun-wu

@qingyun-wu qingyun-wu commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Priority

Level: medium
Reason: completes the multi-server routing of #5977; without it these clients still post with whatever token the process environment holds.

Closes

Follows #5977 and #5978 (both on main); origin/main is merged in, base is main.

Summary

Every gateway client outside room_ops/notify now asks connections.room_credential which connection to use: the room's own server (exact domain match, so a dev.s.space room never goes to s.space), the primary when there is no room. A room no connection serves is refused by name, never answered with another server's token. Installs with one connection keep their existing credential chains (the resolver answers None).

Caller Change
src/owner_room_access.py, src/channel_credentials.py New channel_credentials.room_env(url_vars, token_vars, room): the connection's url|secret in the capability's first token variable, every other credential variable blanked. resolve_credentials(cap, collab_url, room) uses it unless --collab-url is explicit. owner_routing reads the primary row's owner-routing.<instance>.json, falling back to the unsuffixed file. connections.primary_row is a read-only primary lookup (no import, no vault).
pending-questions room DB, crons_table.py Pass the room they open to resolve_credentials.
ensure-cron-room.py, cron-notify.py resolve_token(repo, room) asks the resolver first (room creation uses the primary; a cron-room post uses the room's server). A refusal skips creation / returns "not delivered" with the reason on stderr.
bee_actions.py _gateway_creds(room); post-room resolves for the Bee room, register-room for the primary; a refusal exits 2.
health-check.py --notify-gateway REMOTE_ALERT_ROOM is read from the primary's channel dir first (legacy dir as fallback), and _gateway_creds(room=...) routes by that room. The status readers are untouched.
skills/tutorial/scripts/getting_started.py commons_env(script, room=...) uses room_env; connect_actions applies blanked variables so an outer token cannot shadow the routed one.
Context-first prompt (src/result_markers.py + vendored copy) The fallback is now channel-env.sh <dir> --room <room>, so the file loaded is the room's own connection's. room_ops.py itself already resolves by room from env, channel file or vault. Golden fixture and requeue test updated.
room_ops verbs without a room Documented, not changed: rooms, history, agents, resolve, events list|pull|stream act through the primary (help text, SKILL.md, docs/connections.md).

Before / after evidence

New suite tests/gateway-clients-room-routing.test.py (three connections: ag2.space legacy, s.space primary, dev.s.space; the real resolver end to end; the process's REMOTE_TASK_TOKEN is ag2.space's, so a client that skips the resolver gets the wrong server).

At the parent (04d88fb32, the test files copied in):

Ran 31 tests in 2.636s
FAILED (failures=11, errors=18)

(The two passing cases are the vendored-copy parity check and the room_ops no-room verbs, which #5977 already routes to the primary.)

At this head:

Ran 31 tests
OK

Test plan

  • tests/gateway-clients-room-routing.test.py (new), plus room-ops-room-connection, connections-room-routing, channel-env-resolve, channel-env-order-contract, connections, cron-notify, bee-actions, health-check-notify-gateway, tutorial-getting-started, pending-questions-room-db, pending-questions-room-db-serve-process, pending-questions-single-store, pending-questions-send, ag2space-links, requeue-prelude-rebuild, gateway-task-envelope-golden, result-markers-golden, bridge-marker-no-leak, delivery-primitives-single-owner, codex-core-launcher, hermetic-env-structural, tests/skills/schedule-crons/crons-table.test.py, skills/agent-room-ops/test_room_ops.py: all pass.
  • ruff check on changed files: clean. scripts/lint-hermetic-bridge-tests.py: ok. scripts/review-checks.sh on the diff: PASS.
  • Diff coverage, measured locally on the changed non-test Python lines: 119/120 (the one miss is the sys.path.insert in ensure-cron-room.py when the core src/ is not yet on the path).

Left over

  • pending_questions_room_db._db_link (a display link shown to the owner) still builds its URL from the capability's own order, not the room's connection.
  • mention's handle lookup uses the primary's /v1/agents directory while it posts through the room's connection; unchanged here.

Checklist

🤖 Generated with Claude Code

qingyun-wu and others added 4 commits October 28, 2026 11:18
…oom's server

On an install connected to several homeservers, room_ops, notify and
channel-env.sh always resolved the legacy unsuffixed credential, so an
upload to a room on another server got 403 "not a member" from the
wrong gateway.

connections.connection_for_room/room_credential is now the one owner of
that choice: the row whose server equals the room ID's domain exactly
(never a suffix match), the lane with a task open in a domainless room,
else the primary; no room is the primary. A domain no row serves, a
disconnected row, or a row with no token for its own gateway is refused
by name. Fewer than two rows keeps every caller's existing chain.

room_ops' gateway(room_id), notify's _gateway_config and
channel-env.sh --room delegate to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nnection

The pending-questions and Crons room databases, the tutorial page,
ensure-cron-room, cron-notify, bee-actions and health-check's gateway
alert now ask connections.room_credential which connection to use: the
room's own server (exact domain match), the primary when there is no room.
A room no connection serves is refused, never answered with another
server's token. Single-connection installs keep their existing chains.

- channel_credentials.room_env: the shared answer for clients that read
  credential variables (room_collab / room_commons).
- owner_room_access reads the primary row's owner-routing.<instance>.json,
  falling back to the unsuffixed file.
- connections.primary_row: a read-only primary lookup.
- The context-first prompt passes --room to channel-env.sh (both copies).
- room_ops verbs that name no room are documented as acting on the primary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…paths of the gateway clients

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ils without routing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@qingyun-wu
qingyun-wu requested a review from sonichi October 28, 2026 19:00
Base automatically changed from fix/room-server-connection to main October 28, 2026 19:01
qingyun-wu and others added 2 commits October 28, 2026 12:04
…room-resolver

# Conflicts:
#	docs/connections.md
#	src/connections.py
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

@cla-assistant check

@qingyun-wu
qingyun-wu merged commit b72b5ca into main Oct 28, 2026
39 of 40 checks passed
@qingyun-wu
qingyun-wu deleted the feat/gateway-clients-room-resolver branch October 28, 2026 19:23
sonichi added a commit that referenced this pull request Oct 30, 2026
…eature list and rebuilds when the list is newer

Split out of #5839, which narrows to the on_launch launch-hook capability.
The tour content is data (skills/tutorial/feature-list.json); `publish
--if-newer` rebuilds a page only when the list's revision is newer, and
`publish --all-rooms --if-newer` does that in every room with an entry,
never creating a page. Owner edits are kept; the v2 marker keeps adoption
and recovery safe across versions. Rebased on main: keeps #5980's
room-routed commons_env and #6001's s.space title.

The history and review rounds for this code are on #5839 (squashed here).

Stand: Blue (@chi-chi-air-blue.agent:s.space)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sonichi added a commit that referenced this pull request Oct 30, 2026
…eature list and rebuilds when the list is newer

Split out of #5839, which narrows to the on_launch launch-hook capability.
The tour content is data (skills/tutorial/feature-list.json); `publish
--if-newer` rebuilds a page only when the list's revision is newer, and
`publish --all-rooms --if-newer` does that in every room with an entry,
never creating a page. Owner edits are kept; the v2 marker keeps adoption
and recovery safe across versions. Rebased on main: keeps #5980's
room-routed commons_env and #6001's s.space title.

The history and review rounds for this code are on #5839 (squashed here).

Stand: Blue (@chi-chi-air-blue.agent:s.space)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sonichi added a commit that referenced this pull request Oct 30, 2026
…entity on the room's own connection (#6045)

* fix(tutorial): the ownership tag uses the identity on the room's own connection

On a two-connection install, `publish` routed the room call to the room's
server (#5980) but tagged and recognised pages with the primary channel's
AGENT_MXID. A page the agent made on the other server was then not its
own, and a plain publish created a second Getting started page.

connections.room_agent_mxid (exposed as channel_credentials.room_agent_mxid
beside room_env) names the identity on the row connection_for_room picks:
the row's recorded agent_mxid, else AGENT_MXID in that row's channel env
file, kept only when it names that server. getting_started.main() uses it
(after --agent-mxid), and refuses when the routed connection names none
rather than use another server's identity.

Closes #6044

Stand: Blue (@chi-chi-air-blue.agent:s.space)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(tutorial): a page an earlier run tagged with the primary identity is still this agent's

Pages the unrouted code made carry the primary channel's identity in their
tag while listed by the room's own identity; with the routed identity alone
they still read as not this agent's, and a plain publish would create a
second page. owned() also accepts this agent's other-connection id as the
tag, still only when the lister is this agent (or unnamed).

Refs #6044

Stand: Blue (@chi-chi-air-blue.agent:s.space)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(connections): name the swallowed error when a channel env file cannot be read

_launch_file_value and _channel_dir_value still answer '' (fail closed), and
now log the exception type at debug level; the test pins both lines.

Refs #6044

Stand: Blue (@chi-chi-air-blue.agent:s.space)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
sonichi added a commit that referenced this pull request Oct 30, 2026
…eature list and rebuilds when the list is newer

Split out of #5839, which narrows to the on_launch launch-hook capability.
The tour content is data (skills/tutorial/feature-list.json); `publish
--if-newer` rebuilds a page only when the list's revision is newer, and
`publish --all-rooms --if-newer` does that in every room with an entry,
never creating a page. Owner edits are kept; the v2 marker keeps adoption
and recovery safe across versions. Rebased on main: keeps #5980's
room-routed commons_env and #6001's s.space title.

The history and review rounds for this code are on #5839 (squashed here).

Stand: Blue (@chi-chi-air-blue.agent:s.space)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sonichi added a commit that referenced this pull request Oct 30, 2026
…eature list and rebuilds when the list is newer (#6042)

* feat(tutorial): the Getting started tour renders from a per-release feature list and rebuilds when the list is newer

Split out of #5839, which narrows to the on_launch launch-hook capability.
The tour content is data (skills/tutorial/feature-list.json); `publish
--if-newer` rebuilds a page only when the list's revision is newer, and
`publish --all-rooms --if-newer` does that in every room with an entry,
never creating a page. Owner edits are kept; the v2 marker keeps adoption
and recovery safe across versions. Rebased on main: keeps #5980's
room-routed commons_env and #6001's s.space title.

The history and review rounds for this code are on #5839 (squashed here).

Stand: Blue (@chi-chi-air-blue.agent:s.space)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(tutorial): pin previous.audience for creating, pending and updating

The A.9 audience test built `previous` only under `creating`, so a model
that turned a present-invalid previous.audience into "absent" passed for
`pending` (all five invalid values) and `updating` (four of five). Each
state that carries a previous now gets the full value matrix, and an
accepted value is checked as carried into the converted entry.

Stand: Blue (@chi-chi-air-blue.agent:s.space)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(tutorial): the manifest description names the refresh paths this PR has

Without #5839's on_launch nothing refreshes the page on launch: it is
refreshed on re-link, reinstall or /tutorial, and --if-newer gates it on
the list's revision.

Stand: Blue (@chi-chi-air-blue.agent:s.space)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant