Repository navigation
feat(connections): move to the successor server by reusing the agent's existing secret, falling back to a code - #6037
Conversation
…e token's agent by whoami preview_connection verified an identity only when GET /v1/agents listed one agent. AG2 Space relays list every agent of the token's owner, so a relay token for one of several agents never verified: agent_mxid stayed null and a later start refused with no_identity. The gateway's GET /v1/whoami names the bearer's own agent from its registry; it now settles a multi-agent listing, taken only for an agent of the resolver-registered server. Rows imported from the environment (the legacy row included) never learned an identity. agent-api now runs learn_missing_identities at start and every minute, so GET /api/v1/connections names each row's agent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rner backs off and writes only on a verified preview - A whoami id that a non-empty agent listing does not name leaves the row unverified; the bare-id fallback stays only for an empty listing. - The whoami id is matched whole against a strict Matrix id (lowercase localpart grammar). - One per-row helper (_learn_row_identity) holds the skip rules for both the launch path and the background learner; each passes its own token resolution. - learn_identity runs only for a verified preview, so a pass that verifies nothing leaves connections.json untouched. - run_identity_learner backs a row that did not learn off, doubling from the interval up to an hour, and logs a failed pass by exception type. - Tests: whoami vs listing, strict id, disabled/parked rows, record untouched, backoff. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… into feat/connections-move-reuse-secret
… into feat/connections-move-reuse-secret
…s existing secret, falling back to a code The successor DotSpace's registry began as a byte-for-byte copy of the legacy one's, so an agent neither server has re-coded since holds the same relay secret on both. `move` tries that secret before anyone is asked for a connect code: - successor_code(row): the row's own secret behind the successor's registered gateway host (same path), from the dotspace LEGACY_SERVER/SUCCESSOR_SERVER mapping; never printed. - try_reuse_move(instance): previews that code (whoami included); proceeds only when the successor verifies the old row's agent localpart on the successor server. Then it adds the successor row (skip_backlog), promotes it, re-points the agent MCP descriptor (state/ag2-mcp.json: mint/MCP URLs, agent_id, a 0600 env_file holding the code, the old descriptor kept as .bak-<legacy slug>) and mirrors each legacy worker-room binding to its successor twin through an injected roster (room_bindings_from_script; core never names the skill). Refusals write nothing: no_successor, no_token, auth_rejected, unverified, agent_mismatch, successor_exists. - CLI `connections.py move [INSTANCE] --repo --logs-dir [--dry-run]`, printing the code fallback on a refusal; `POST /api/v1/connections/move` for the desktop. - The sspace-migration health hint suggests `move` first; docs/connections.md describes it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sonichi
left a comment
There was a problem hiding this comment.
Requesting changes at 12ae0cb (agent-assisted, Blue for Chi's seat). The secret handling is sound:
- The successor host comes only from the resolver (legacy → configured successor → registered
gateway_host), with no new literals. - The secret goes out only in the
Authorizationheader over the pinned probe, and never into output, URLs or errors. - The move requires the successor to verify the same localpart on the matching server.
- The route uses the same
bearer_request_allowedgate as the other routes. - Writes are atomic and 0600.
The four changed test files pass locally (324, 21, PASS and 7). What I'd fix:
- The re-pointed descriptor keeps the old
relay, so the MCP proxy refuses the bearer.repoint_mcp_descriptorupdatesenv_file,env_key,agent_id,mcp_urlandmint_url, but notrelay(orbearer_digest). The proxy'sfallbackAllowedrefuses the bearer from the descriptor's own key when the token's URL half (now the s.space relay) doesn't matchd.relay(still ag2.space). So the move reportsmcp: repointedand every MCP call then has no bearer. The test fixture omitsrelayandbearer_digest, so the suite can't see it. Updaterelayto the successor's (and recompute or dropbearer_digest), and add both fields to the fixture. - Which descriptor is this? This writes
<workspace>/state/ag2-mcp.json, but on desktop installs the descriptor is$APP_SUPPORT/channels/ag2-mcp.json, written by the host (agent_mcp.rs). Nothing in this repo writes thestate/path. Please name the component that readsstate/ag2-mcp.jsonfor terminal-run cores. If the host owns the file, a second writer here will be overwritten, or will quietly reportmcp: absenton desktop. - A promote failure leaves a half-move. After
add_connection, a refusedpromotekeeps the s-space row. A retry hitssuccessor_exists, and the CLI says to runpromoteby hand. But a manual promote never re-points the descriptor or mirrors room bindings, so they stay on ag2.space. Letmoveresume when the existing successor row is the same agent. - An
OSErrorfrom the descriptor or env write after promote isn't caught: you get a traceback (or a 500), the row is promoted, and rooms aren't mirrored. - Low:
_env_file_withwritesKEY=https://…|secretunquoted, so anything that shell-sources it runs the part after|as a command. Quote it.- Compare
ownerfrom the preview too. - The refusal when ag2.space can no longer verify the old agent says
agent_mismatch;unverifiedis the accurate reason. pool_room_bindingsis copied identically inscripts/connections.pyandsrc/agent-api.py; put it in one place.
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 94%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
|
…nreachable no_token branch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Re-checked at bf8bba1 (agent-assisted, Blue for Chi's seat). The new commit adds tests for the refusal and fallback paths and drops the unreachable
Coverage is also at 93% against the 95% bar. |
…lf-move, and reports instead of raising Review follow-up: - The re-pointed MCP descriptor's `relay` moves to the successor (the MCP proxy refuses a bearer whose relay differs). `bearer_digest` is kept when it hashes this secret, else dropped. - The descriptor is the one the core's Claude Code MCP config names (AG2_MCP_DESCRIPTOR) inside the workspace, else state/ag2-mcp.json; the desktop host's own descriptor is never touched. - A successor row for the same agent (a move whose promote failed) is resumed: promoted, the descriptor re-pointed, rooms mirrored. Another agent's row is still successor_exists. - A failed descriptor or env-file write is reported as mcp: write_failed; rooms still mirror. - The env file value is single-quoted, so sourcing it never runs what follows the `|`. - The owner is compared too; an old gateway that no longer confirms the agent is `unverified`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@cla-assistant check |
|
Addressed in 6826536, on top of a merge of main (#6029 has landed, so the diff is now the move change alone):
|
sonichi
left a comment
There was a problem hiding this comment.
Approving at 6826536 (agent-assisted, Blue for Chi's seat). Every item from my review is addressed:
- Relay:
relayis re-pointed to the code's gateway (and counts toward "on legacy").bearer_digestis kept only when it equalssha256(secret), matching the proxy's "secret half" digest, and dropped otherwise. - Descriptor:
mcp_descriptor_pathtakes theAG2_MCP_DESCRIPTORthe core's Claude Code config names inside the workspace, elsestate/ag2-mcp.json. The desktop host's descriptor outside the workspace is never touched, which settles the second-writer question. - Resume: a successor row for the same agent is resumed (promote, re-point, mirror), and another agent's row is still
successor_exists. - Write failures: they now return
mcp: write_failedinstead of raising, and rooms still mirror. - Smaller items: the env value is single-quoted, the owner localpart is compared, and
unverifiedis used when the old gateway no longer names the agent.
The changed test files (connections, connections-cli, health-check migration) pass locally. CI was still running; coverage needs to clear 95% before merge.
Priority
Level: medium
Reason: terminal-run cores still on the legacy DotSpace need a way to move without a new connect code whenever their existing secret still works.
Closes
Summary
The successor DotSpace (s.space) started from a byte-for-byte copy of the legacy (ag2.space) agent registry, relay bearers included. So an agent that neither server has re-coded since holds the same secret on both.
connections.py move(andPOST /api/v1/connections/movefor the desktop) tries that secret first:successor_code(row)puts the row's own secret behind the successor's registered gateway host (same path). The successor comes from the existing dotspaceLEGACY_SERVER/SUCCESSOR_SERVERmapping and the resolver. No new host literals are added, and the secret is never printed.try_reuse_move(instance)previews that code with whoami (fix(connections): a relay that lists the owner's whole fleet names the token's agent by whoami #6029). It continues only when the successor verifies the old row's agent localpart on the successor server. If the old row has not learned its agent yet, a preview of its own token supplies it. It then:skip_backlog, so the moved bridge settles the backlog stamped before it, as in feat(connections): a connection added with skip_backlog settles tasks stamped before it #6032);promote, so the old bridge keeps running as a secondary untilpark-legacy;state/ag2-mcp.json: mint and MCP URLs from the successor's DotSpace entry,agent_idset to the moved agent, andenv_filepointing at a 0600 channel env file holding the code, written atomically. The old descriptor is kept asag2-mcp.json.bak-<legacy slug>;bind_room. Core never names or imports the skill: the adapters pass the roster script path toroom_bindings_from_script, which returns None when the script or its roster is absent. A twin that is already bound is left alone, and a refused binding is reported without undoing the move.no_successor,no_token,auth_rejected,unverified,agent_mismatch,successor_exists. The CLI then prints the code fallback (get a connect code at the successor's agent portal, thenconnections.py add).--dry-runchecks the secret and writes nothing.sspace-migrationhealth hint now suggestsmovefirst.docs/connections.md("Moving a terminal-run core to s.space") describesmovefirst and the code path as the fallback.Builds on #6029 (whoami settles identity), now merged; main is merged in, so the diff is the move change alone. #6031 and #6032 are already on main and are used as merged.
Checklist
gh pr list --state open)cb33e459); output belowscripts/review-checks.sh: PASS)claude_home_path/ the workspace helpers; no new path resolutionBefore / after evidence
New tests at the parent (#6029's head
cb33e459, the source without this change):At HEAD:
Live read-only check on a host whose legacy agent was re-coded on s.space. As expected, the reuse is refused and nothing is written (the state files' checksums are the same before and after):
Test plan
tests/connections.test.pyMove:not_beforeset, vault keyAG2_REMOTE_TOKEN_S_SPACE) and makes it primary with its bridge running;auth_rejectedleaves the workspace and the vault byte-identical and starts no bridge;agent_mismatch, an unverified answer givesunverified, and a server without a successor givesno_successorwith nothing probed;no_token, and an existing successor row givessuccessor_exists;🤖 Generated with Claude Code