Skip to content

fix(connections): each named lane runs as its own row's agent, never the legacy one it inherited - #5978

Merged
qingyun-wu merged 2 commits into
mainfrom
fix/lane-launch-identity
Oct 28, 2026
Merged

qingyun-wu merged 2 commits into
mainfrom
fix/lane-launch-identity

Conversation

@qingyun-wu

Copy link
Copy Markdown
Collaborator

What changed, and why

After an install moves from ag2.space to s.space, the s.space lane's bridge runs as the ag2.space agent.

  • src/startup-runtime.sh start_gateway_lanes (L459-464 on main) sources the legacy channel env under set -a, so its AGENT_ID/AGENT_MXID are exported to every named lane launched after it.
  • src/connections.py launch_env (L1841-1860 on main) copies that environment unchanged; it clears neither the identity variables nor AG2_DEVICE_ENV.
  • The bridge's identity chain, ag2_sparrow.agent_identity.resolve, reads the process env first.

The lane on the other homeserver therefore reports the legacy agent. Its delivery check refuses its own rooms (proactive posts strand or park), it stamps the wrong receiving instance, and it advertises the worker-pool profile under the wrong id.

Fix (the policy lives in launch_env):

  • A named entry's identity is pinned to its row's agent_mxid through agent_identity.launch_environ, the helper the desktop (Hatch) launch path already uses. It drops the inherited AGENT_ID and sets AG2_IDENTITY_PINNED + AGENT_MXID, so the bridge reads no identity from channel files or the host binding.
  • AG2_DEVICE_ENV (the legacy row's credential file) is dropped for named entries.
  • Plan entries now carry the row's agent_mxid. Nothing names a server; each row brings its own agent, so any number of homeservers (ag2.space + s.space + dev.s.space) select correctly.
  • A row with no recorded agent first learns it from a verified preview of its own token (the same learning reconnect does). An env-only launch takes it from that preview in memory. A lane still without an agent is refused (NoIdentity, launch record reason: no_identity, named in remote-gateway-bridge.log), never started under the inherited identity.
  • The legacy (unsuffixed) row's environment is unchanged.

Behavior change to note: a named lane whose gateway cannot confirm an agent (unreachable, or a host the DotSpace resolver does not register, since a preview only establishes identity on a verified listing) no longer starts. It used to start under whatever identity it inherited. Reconnecting it once its gateway answers brings it up.

Look first

src/connections.py: launch_env, _learn_missing_identity / _verified_agent, and the two new lines in launch.

Docs

docs/connections.md Startup section: the identity pinning and the no_identity launch reason (catalog.json already shows today's last_verified).

Before / after

A repro that builds launch_env for an s.space row while a legacy identity is in the environment:

origin/main:
{'AGENT_MXID': '@agent:ag2.space', 'AGENT_ID': '@agent:ag2.space', 'AG2_DEVICE_ENV': '/legacy/channels/ag2space/.env'}
HEAD:
{'AGENT_MXID': '@agent:s.space', 'AGENT_ID': '<unset>', 'AG2_DEVICE_ENV': '<unset>'}

The new LaneIdentity tests run against main's connections.py:

ERROR: test_a_row_without_an_agent_is_refused_not_inherited
ERROR: test_launch_learns_a_missing_agent_from_the_rows_own_token
FAIL: test_each_named_lane_carries_its_own_agent_and_no_device_env
AssertionError: '@legacy:ag2.space' != '@me:s.space'
FAILED (failures=1, errors=2)

At HEAD: Ran 268 tests ... OK for tests/connections.test.py.

Tests

  • tests/connections.test.py, new LaneIdentity class:
    • an inherited legacy AGENT_ID/AGENT_MXID/AG2_DEVICE_ENV, with launch_env built for an s.space row and for a dev.s.space row: each gets its own AGENT_MXID, AGENT_ID and AG2_DEVICE_ENV are absent, and agent_identity.resolve answers with the row's agent even when a channel file names the legacy one
    • the legacy row is unchanged
    • a row with no identity raises NoIdentity, and launch records no_identity without spawning
    • launch learns a missing agent from the row's own token
    • an unpreviewable token names no agent
  • tests/startup-gateway-channel-dir.test.sh drives the real start_gateway_lanes block. It now exports a legacy identity and device env and asserts that the lane runs as its row's agent without them, and that an env token with no recorded agent is refused and reported.
  • Updated expectations: tests/connections-cli.test.py (env-only and rotation without a named agent now refuse) and tests/connections-no-cloud-trust.test.py (records an agent for its unregistered fixture gateway).
  • Passing: connections, connections-cli, connections-no-cloud-trust, agent-api-connections, runtime-api-connections-view, health-check-supervised-bridge-restart, ag2-sparrow-agent-identity, and the shell tests startup-gateway-channel-dir, startup-runtime-connections-launch-plan, gateway-bridge-wrapper-primary-room, gateway-bridge-wrapper-self-heal.
  • tests/health-check-fix-down-bridges.test.py case f fails identically on origin/main on this host. It is environment-dependent (a live slack bridge) and unrelated.
  • ruff check, scripts/lint-hermetic-bridge-tests.py, scripts/lint-workspace-resolution.sh --diff: clean. Diff coverage of src/connections.py (diff-cover vs origin/main): 100%.

Live path

This is the startup launch path. No post-restart round trip has been captured yet, because restarting the owner's live lanes from this session was out of scope. The real launcher block is exercised by startup-gateway-channel-dir.test.sh. A round trip on a host with both an ag2.space and an s.space lane is still owed before approval.

Overlap

#5911 also edits launch_env: it clears the URL keys in place of REMOTE_TASK_URL. The two changes are adjacent and should merge mechanically. This PR stays out of the shared room-tool connection resolver work.

🤖 Generated with Claude Code

…the legacy one it inherited

start_gateway_lanes loads the legacy channel env with `set -a`, so AGENT_ID / AGENT_MXID
reach every named lane launched after it. launch_env copied the environment unchanged,
and the bridge's identity chain (agent_identity.resolve) prefers the process env, so a
lane on another homeserver ran as the legacy agent: its delivery check refused its own
rooms, it stamped the wrong receiving instance and advertised the wrong agent id.

launch_env now pins a named entry's identity to its row's agent_mxid through
agent_identity.launch_environ (the helper the desktop launch path uses) and drops
AG2_DEVICE_ENV, the legacy row's credential file. The plan entry carries the row's
agent_mxid. A row with none learns it from a verified preview of its own token before
launch (env-only: in memory); one still without is refused as `no_identity` rather
than inheriting. The legacy row's environment is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@qingyun-wu
qingyun-wu requested a review from sonichi October 28, 2026 18:42
@github-actions

Copy link
Copy Markdown
Contributor

Coverage Gate

✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 94%.

Diff Coverage

Diff: origin/main...HEAD, staged and unstaged changes

  • src/connections.py (100%)

Summary

  • Total: 42 lines
  • Missing: 0 lines
  • Coverage: 100%

@github-actions

Copy link
Copy Markdown
Contributor

@cla-assistant check

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