Repository navigation
feat(ag2-sparrow): ag2-sparrow-claude-local connect --base --code - #5860
Conversation
…ver connects as the core REMOTE_TASK_TOKEN_FILE becomes the first token source: readable, it closes the channel .env and vault tiers; refused or missing, it closes every tier and is fatal. A seat (REMOTE_TASK_SEAT, set by the launch environment) reads only its own .env and token file, and its identity is strict: never the host's binding. The credential owner gains write_seat_credentials (the <seat>/channels/ag2space .env + token, 0600, temp + rename) and seat_bridge_environ (exports GATEWAY_INSTANCE, REMOTE_TASK_TIER, REMOTE_TASK_TOKEN_FILE, clears inherited token, URL and identity variables, caps the tier at the core's). claude-local's bridge_env applies it when --token-env-file names a seat .env. Worker-identity plan section 9.1 items 1-3 and the S3 writer. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 94%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
packages/ag2-sparrow/ag2_sparrow/adapters/claude_code/cli.pyLines 417-425 417 if _dirs.bridge_host():
418 return (f"this host's bridges are run by {_dirs.bridge_host()}, whose worker pool owns each agent's seat; "
419 f"{COMMAND} connect is for a host with no core")
420 if _dirs.task_dir_configured():
! 421 return "AGENT_CONNECT_TASK_DIR is set; connect wires the agent's own root, so unset it first"
422 claude = shutil.which(args.claude) or args.claude
423 plugin = Path(args.plugin_dir).expanduser().resolve()
424 if not (plugin / ".claude-plugin" / "plugin.json").is_file():
425 return f"{plugin} is not a Claude Code plugin (no .claude-plugin/plugin.json)" |
…at cannot start says why and backs off - REMOTE_TASK_TOKEN_FILE is read with bounded_file_read.read_private: O_NOFOLLOW, this user's own, no group or other bits; any refusal fails closed. A channel .env standing in as the rotation file keeps read_regular. - A fatal start writes gateway-status with the reason before exiting; a seat then waits SEAT_FATAL_BACKOFF_S, so launchd KeepAlive or systemd Restart=always retries it about once a minute, not hot. - A token file holding no token, or a seat token with no gateway URL, is fatal with its own reason instead of the generic "set REMOTE_TASK_TOKEN". - A 32-hex GATEWAY_INSTANCE is a seat too; a seat ignores an inherited REMOTE_TASK_URL. - Tests for each, and for the lines diff coverage reported. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qingyun-wu
left a comment
There was a problem hiding this comment.
Approved at 56c5ce9 — agent-assisted review by Mars, Qingyun's Sutando seat, on Qingyun's instruction to review and approve Chi's PRs.
Reviewed this PR's own commits over #5859 (already approved at 2b846d2): connect_preflight/connect, the shared session_options, and the connect section of connect_common. Ran test_claude_code_adapter, gateway-credential-sources, test_adapter_boundaries, test_codex_local_connect, test_hatch_served_setup, ag2-sparrow-agent-identity and gateway-relay-result-no-ceiling at this head under env -i: all OK.
- Preflight and code/base checks run before any request, and every refusal before redemption says the code wasn't used.
- The redeemed bearer and relay_url are registered as secrets before validation can fail. The server-supplied agent_id only reaches the filesystem through
root_segment(path-safe). relay_url and bearer are re-validated bywrite_seat_credentials(no '|', whitespace or control chars) before<url>|<bearer>is written 0600. - The POST now runs inside
gateway_call, and install is reused, not copied.
Non-blocking:
--baseaccepts plain http for any host (gateway_url_problemallows http), so the one-time device_code can go out in cleartext. I'd limit http to loopback for connect.- A 200 with a malformed body spends the code. The message says so, which is the best that can be done client-side.
- Merge order: #5859 first, then rebase this one, as the body says.
56c5ce9 to
bc22985
Compare
liususan091219
left a comment
There was a problem hiding this comment.
Comment at a8dd49e8. The round-1 fixes check out, but I am not approving: Qingyun's approval is at 56c5ce9b, and this branch has since been rebased onto #5859's new head (2f6389bf). It also gained five commits after that approval (8e6083a0…a8dd49e8). No approval exists at this head.
What I checked (this PR's commits over #5859)
ConnectCode: 16 tests pass, including https-only base/relay, an off-domain relay never written,--trust-relay-host, a 202 retried withretry_after, 404gonevsagent_revoked, the code off argv (stdin/getpass), and the bearer absent from every argv.gateway-credential-sources(169),test_adapter_boundaries(10) andtest_codex_local_connect(35) also pass.- Mutation: with
relay_problemmade to return""(connect_common.py:1093), 3ConnectCodetests fail. With it reverted, they pass. - Probe of
relay_problem:https://relay.ag2.spaceunderhttps://ag2.spaceis accepted.http://relay.ag2.spaceandhttps://evil.exampleare refused.evil.examplewith--trust-relay-host evil.exampleis accepted. - I did not run the tmux-backed tests in
test_claude_code_adapter.py. This review does not touch a tmux server, so here they report 11 failures and 13 errors, all on a missingtmux. CI runs them. - Hardcoded-path scan over the added lines: clean.
Non-blocking
_host_family(connect_common.py:1086-1090) takes the last two labels, so under a multi-label public suffix the check is a no-op. In my probe,https://evil.co.uk/rwas accepted for--base https://api.foo.co.uk. AG2 Space hosts are*.space, so this does not bite today. A public-suffix-aware match, or an exact-host default, would make the docstring's "registrable part" true.
Gates
- CI: 36 pass, 3 skipped.
license/cla: success. No unresolved threads.
What is still missing
- A fresh approval at this head. Qingyun's approval at
56c5ce9bpredates the rebase and the round-1 fixes. - #5859 merges first. It is open at
2f6389bf, and the body says to rebase this PR after it. - The live witness: a real
connectagainst AG2 Space with a fresh code, ending in a seated bridge that polls. It is named, not blocking.
— Sutando (susans-MacBook-Pro)
qingyun-wu
left a comment
There was a problem hiding this comment.
Re-review at a8dd49e — agent-assisted review by Mars, Qingyun's Sutando seat. My approval at 56c5ce9 doesn't carry over to this head.
This PR's own commits are good (4d724cc → a8dd49e). test_claude_code_adapter, gateway-credential-sources and test_codex_local_connect pass under env -i.
- https is required, with plain http only on loopback. That resolves my earlier note about the device_code going out in cleartext.
- The code stays off argv (
--code -or a prompt, and a warning on--code VALUE), andcode/trust_relay_hostare stripped beforeinstall. relay_problemruns before anything is written, so a refused relay URL never reaches disk.- A 202 is retried honouring
retry_after, clamped to 1–30 s with bool/NaN rejected, and a 404 tellsagent_revokedapart from a used or expired code.
Changes requested only because of the stack. This branch contains #5859's 2f6389b (git merge-base --is-ancestor confirms it), which carries the core-launcher regression in my and Susan's #5859 reviews. If this PR merged first, that regression would land with it. Please rebase onto the fixed #5859 and I'll approve.
Non-blocking: _host_family takes the last two labels, so on a two-label public suffix (*.co.uk, *.com.au) any relay under the suffix would match. That doesn't matter for ag2.space, but it's worth a comment, or the PSL if a customer base ever lives under one.
…named channel .env reads as before startup-runtime.sh names the core's own channel .env as REMOTE_TASK_TOKEN_FILE, so the private read (0600, owner, no symlink) applied to any named file would fail a live core whose .env is 0644 or a symlink. read_private now applies when the environment is a seat's; a host reads the file with read_regular, as before. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The two-step setup AG2 Space shows for a Claude Code agent. connect checks the CLI version, tmux, the plugin and the Claude login before the single-use code is spent; redeems it at <base>/connect/poll (503 retried with backoff, 404 means used or expired: make a new one); has the credential owner write the seat beside the agent's root (<root>.bridge/channels/ag2space, .env + token, 0600, GATEWAY_INSTANCE minted once and kept, tier owner); then runs install with that seat as the token env file. The bearer is registered as a secret on receipt and never printed or put on a command line. The served setup is used when setup_fetch returns one, else the built-in. A host whose bridges a supervisor runs is refused: its worker pool owns the seats. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every URL-taking call in the package runs inside gateway_call, so what it raises is scrubbed of the URL; the redemption was the one outside it. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tries a 202
- The code is read from stdin (--code -) or asked for on a terminal; a code
on the command line still works, with a warning that ps and shell history
keep it.
- --base and the returned relay URL must be https (plain http only on a
loopback host); a relay outside --base's domain is refused unless named
with --trust-relay-host, and a refused relay URL is never written.
- A 202 {state: redeeming, retry_after} is retried like a 503, honouring
retry_after up to 30 s; a 404 names agent_revoked apart from gone.
Stand: Echo Act IV Pro
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e-error answers Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nects Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two hosts sharing their last two labels are not one owner's under a public suffix such as co.uk; with no public suffix list in the stdlib, connect accepts the base's own host or a host under it, else --trust-relay-host. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
a8dd49e to
79912e4
Compare
qingyun-wu
left a comment
There was a problem hiding this comment.
Approve at 79912e4 — agent-assisted review by Mars, Qingyun's Sutando seat, on Qingyun's instruction to review and approve Chi's PRs. This replaces my changes-requested at a8dd49e.
- Stack: rebased onto 3f1808f (
merge-base --is-ancestorconfirms it), so it now carries the fixed #5859. - My non-blocking note is addressed:
_host_familyis gone._relay_host_okaccepts the relay only on the base host itself or a host under it (== baseorendswith('.' + base), trailing dot stripped, lowercased), otherwise--trust-relay-host. That's stricter than a shared parent, and correct without a PSL. Tests cover the co.uk sibling, a host under a co.uk base, and the same host. - Tests: test_claude_code_adapter, gateway-credential-sources, src/remote-gateway-bridge and test_codex_local_connect all pass at this head under
env -i. The rest of the connect review stands as before.
Merge #5859 first. The CLA check hadn't posted on this head when I looked.
|
@cla-assistant check |
liususan091219
left a comment
There was a problem hiding this comment.
Approve at 79912e42. Qingyun approved at this head. I verified the one commit added since my comment at a8dd49e8. Before merge, the branch still needs a rebase onto main, and #5859 has to merge first.
What changed since a8dd49e8
git range-diffshows the six connect commits unchanged across the rebase onto #5859's3f1808fa. One commit is new:79912e42replaces_host_familywith_relay_host_ok.- The relay is now accepted only on the base's own host or a host under it. This resolves my non-blocking
.co.uknote.
What I checked
ConnectCode+RedeemEdges: 19 pass.gateway-credential-sources: 170 pass.test_codex_local_connect: 35 pass.test_adapter_boundaries: 10 pass. All run underenv -iwith a scratch HOME.- Mutation 1. I put the old last-two-labels match back into
_relay_host_ok(connect_common.py). The new relay test then fails 3 subtests:evil.co.uk,relay.example.co.ukunderapi.example.co.uk, andnotexample.co.uk. With the mutation reverted, all tests pass. - Probe of
relay_problemagainst basehttps://chat.ag2.space/api:- Accepted:
chat.ag2.space,CHAT.ag2.space.andx.chat.ag2.space. - Refused:
evilchat.ag2.space,chat.ag2.space.evil.com,relay.ag2.space,http://chat.ag2.space, and the userinfo formchat.ag2.space@evil.com. - The real flow keeps working, because the bridge derives the provision API from the relay host itself.
- Accepted:
- The stacked #5859 fix. My #5859 change request at
2f6389bfasked that only a seat read its token file private.3f1808fa(contained here) does exactly that. - Mutation 2. I dropped
is_seat(...)from_read_token_file_text(remote_gateway_bridge.py). The new host-env test then fails for0644andsymlink. With the mutation reverted, it passes.
Gates
- CI: 36 pass, 3 skipped.
license/cla: success (1 context).- No unresolved threads.
Still owed (named, not blocking this approval)
- Merge conflict with main. The branch is
CONFLICTING. The conflict is inpackages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py, and #5859's head3f1808fahits the same one against main. The rebase will move the head, so approvals will need refreshing there. - Merge order. #5859 merges first, then this PR. #5859 itself is still
CHANGES_REQUESTEDon GitHub, since my request there predates3f1808fa. - The live witness. A real
connectagainst AG2 Space with a fresh code, ending in a seated bridge that polls.
Non-blocking
- A backend whose relay sits on a sibling host (say
relay.ag2.spaceunderapi.ag2.space) now needs--trust-relay-host. That is the stated intent, but worth knowing if the hosts are ever split.
— Sutando (susans-MacBook-Pro)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
liususan091219
left a comment
There was a problem hiding this comment.
Comment at 94ddf46b, not approving yet: no other approval exists at this head. Qingyun's approval and mine are both at 79912e42. The only change since then is 94ddf46b, a merge of main (56bd959b) into the branch, and I re-verified it below. The missing piece is a re-approval from another reviewer at this head; once that lands, this is ready to approve.
What changed since 79912e42
- One merge commit.
git show --remerge-diffshows one conflict, inpackages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.pyat the package import block. The resolution is the union of both sides: main'ssetup_fetchimport and this branch'sread_private. Nothing else was hand-edited. Three other overlapping files (packages/ag2-sparrow/README.md,tests/test_hatch_served_setup.py,src/remote-gateway-bridge.test.py) auto-merged. read_privateis still used by_read_token_file_text, so dropping it in the resolution would have been a NameError, not a silent change.
What I checked at 94ddf46b (env -i, scratch HOME)
ConnectCode+RedeemEdges: 19 pass.test_codex_local_connect+test_adapter_boundaries+test_hatch_served_setup: 271 pass.test_task_lifecycle: 282 pass.gateway-credential-sources: 170 pass.src/remote-gateway-bridge.test.py: all green.- Full
test_claude_code_adapter: 24 failures, allLauncherTmux. The identical 24 fail at79912e42under the same env (no tmux on that PATH), so they are my instrument, not the merge. - Mutation on the conflicted file. Dropping
owner.is_seat(...)from_read_token_file_textmakestest_a_host_reads_its_named_channel_env_as_before_and_only_a_seat_reads_it_privatefail for0644andsymlink. Reverted, all 170 pass.
Gates
license/cla: success (1 context).- CI: two red, one cause.
python standalone tests (6/9)fails only intests/skills/process-hygiene/reap-orphans.test.py("the orphan was not a candidate"). That is the open flake #5733, and this diff does not touch process-hygiene.tsc + tests (clean install)fails only because it gathers that job's result. A re-run should clear it. - No unresolved threads.
Still owed (named, not blocking)
- Merge order. #5859 is still open. Its head has moved to
158a257c(a main merge plus one test commit,158a257c1, pinning the named token file over the channel.envand vault), and this branch does not contain it. After #5859 merges, this branch needs a rebase or merge onto main, and that moves the head again. - The live witness. A real
connectagainst AG2 Space with a fresh code, ending in a seated bridge that polls.
— Sutando (susans-MacBook-Pro)
#5859 landed on main as f399c4c. The three conflicted files (src/ and vendored gateway_credential_sources.py, tests/gateway-credential-sources.test.py) take main's version: it is a strict superset of this branch's copy of #5859, and this PR adds nothing to them. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@cla-assistant check |
The Hatch held-task line (#5828), Hatch self-upgrade to the served target_version (#5676), the served-setup fetch from the broker (#5855), served Hatch wording (#5665), seat-bridge env and token fixes (#5893, #5859), and the claude-local/codex-local connect commands (#5860, #5761). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
What
ag2-sparrow-claude-local connect --base <url> --plugin-dir <dir>(it asks for the code, or reads it with--code -) covers the local half of AG2 Space's two-step "Add a Claude Code agent" flow. This is S4 for claude-local, plus the S3 redeem-and-seat step inconnect_common.Stacked on #5859 (the seat token file). This branch carries #5859's commits first, so review only the commits after them. Merge order: #5859 first, then this one, rebased onto main.
In order, it does the following:
AGENT_CONNECT_TASK_DIRset.claude auth statusunder--claude-config-dir.--baseor a code that does not start withacp.is refused first.POST <base>/connect/poll {"device_code": ...}(connect_common.redeem_code):relay_url,bearerandagent_id. The bearer and URL go into the processSecretRegistrythe moment they are parsed.connect_common.write_seatcalls the credential owner'swrite_seat_credentials, from fix(ag2-sparrow): a seat bridge reads its own token file first and never connects as the core #5859, withtier=owner. It writes<root>.bridge/channels/ag2space/{.env,token}, both 0600, where<root>is the agent's own root under the agents dir.GATEWAY_INSTANCEis kept from an existing seat.envand minted (32 hex) only when there is none. A reconnect rewritestokenthrough a rename.connectcallsinstall()with--token-env-file <seat>/.env. No install step is copied, and the argument parser shares onesession_options()between the two commands. fix(ag2-sparrow): a seat bridge reads its own token file first and never connects as the core #5859'swire_bridgethen exports the seat and clears the inherited token, so the bridge runs as the seat alone.connectaskssetup_fetch.fetch_setup("acp", "claude"). It printssetup: served revision Nwhen a setup comes back, elsesetup: built-in. On main no transport is installed yet (feat(ag2-sparrow): Sparrow fetches its served setup from the broker when a heartbeat announces a new revision #5855 adds it), so today it always prints the built-in line.The bearer never appears in output, argv, the unit, the config or the seat
.env. The only file that holds it is the seat'stoken.Review round 1 (Mini's blockers)
--baseand therelay_urlthe backend returns must be https. Plain http is allowed only on a loopback host, for tests. A relay must be on--base's own host or a host under it; anything else is refused unless the owner names that exact host with--trust-relay-host. Sharing the last two labels is not enough, because without the public suffix list (the stdlib has none)a.co.ukandb.co.ukwould look like one owner's. The real flow keeps working: the relay basehttps://chat.ag2.space/relayand its provision APIhttps://chat.ag2.space/apishare a host. A refused relay URL is never written. The code is spent by then, and the error says so.--code -reads one line from stdin. With no--code, connect asks on a terminal throughgetpass, and refuses (no request made) when stdin is not a terminal.--code VALUEstill works but warns that ps and shell history keep it. The README's setup command uses the prompt or stdin form.{state: redeeming, retry_after}is retried like a 503, honouringretry_afterclamped to 1 to 30 s, on the same attempt budget. A 404 body witherror,reasonorstateofagent_revokedsays the agent was removed.gone, or anything else, says the code was used or expired.gateway_call, as the package's URL guard requires.Review first
packages/ag2-sparrow/ag2_sparrow/connect_common.py: the new section at the end.adapters/claude_code/cli.py:connect_preflightandconnect.Docs
packages/ag2-sparrow/adapters/claude-code/README.md(Launch section) documentsconnect.Not in this PR
ag2-sparrow-codex-local connect. codex-local'sinstalltakes--workand runs no bridge of its own; it relies on a bridge someone else runs. So there is no install-side bridge wiring for connect to reuse, and a codex connect today would spend the code and leave nothing polling. The follow-up is to lift claude-local'swire_bridge/bridge_env/bridge_homeintoconnect_commonso both adapters share it, then add codexconnecton top.--plugin-diris still required.builtin-setup.claude.json) and the servedverifyhints. These wait on feat(ag2-sparrow): Sparrow fetches its served setup from the broker when a heartbeat announces a new revision #5855's transport.--code, connect now asks for one.Tests
Each file below was run with
env -i HOME=$HOME PATH=$PATH nice -n 19 python3 <file>on this head, and all pass:packages/ag2-sparrow/tests/test_claude_code_adapter.py(251, the 10 newConnectCodecases included),tests/gateway-credential-sources.test.py(164, the credential-owner guard),packages/ag2-sparrow/tests/test_adapter_boundaries.py,test_codex_local_connect.py,test_wheel_contents.py, andtests/ag2-sparrow-claude-local-wheel.test.py.scripts/review-checks.shpasses.Before this PR,
ag2-sparrow-claude-local connectwasinvalid choice: 'connect'. After it, the cases below pass.packages/ag2-sparrow/tests/test_claude_code_adapter.py,ConnectCode, covers:.env;goneandagent_revokedtold apart;redeeming: retried honouringretry_afterwith a cap, and given up with a clear message;.co.uksibling included), never written, and accepted with--trust-relay-host; an http base refused unless loopback;redeem_codeedges: unreachable, malformed 200, unreadable error body, other HTTP status, andretry_afterbounds;🤖 Generated with Claude Code