fix(gateway): auth-rejection recovery — hot-reload a rotated token instead of crash-looping - #2323
Conversation
…stead of crash-looping On 401/403 the bridge exits FATAL. Under a supervisor that blindly relaunches, a revoked or expired bearer becomes a silent ~5s crash-loop hammering the gateway until a human notices — which also makes key expiry policies unsafe to enable. When REMOTE_TASK_TOKEN_FILE names the durable token source (dotenv-style or the raw onboarding string), the bridge now re-reads it on auth rejection: a different token is swapped in live (poll loop reads the TOKEN global per request; the event channel and card poster share one auth-header dict held by reference), an unchanged one holds a slow re-check loop (REMOTE_AUTH_RECHECK_INTERVAL, default 30s) with the poller singleton kept heartbeated, so one live waiting process replaces the crash-loop and re-running the connect flow resumes the bridge with no restart. Unset env → exactly the previous FATAL-exit behavior. startup.sh exports REMOTE_TASK_TOKEN_FILE alongside sourcing the relay env file, only when that file exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DjCFofrj7FTrFnhjMidJ2Y
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 75%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
|
qingyun-wu
left a comment
There was a problem hiding this comment.
Blocking finding:
- [P1] The event channel can still die permanently during the auth-rejection recovery window.
remote_gateway_bridge._recover_auth()now keeps the main poller alive while it waits for the token file to rotate and then mutates_AUTH_HEADERSin place, butEventChannel._consume_once()still treats 401/403 as fatal and returnsFalse;EventChannel.run()then exits. In a real expired/revoked-token window, the SSE channel can reconnect with the stale bearer, get 401/403, and stop before the main poller hot-swaps the token. After_recover_auth()resumes, nothing restarts_EVENT_CHANNEL, so workspace events and human-action card delivery stay dead until the whole bridge process restarts. The shared header dict only helps if the channel is still running; the currenttest_channel_fatal_auth_stopsbehavior confirms it is not. Please either route the event channel through the same token-rotation wait/retry path or restart it after a successful reload.
Focused checks run in an isolated /private/tmp checkout:
PYTHONPYCACHEPREFIX=/private/tmp/sutando-pr-2323.pycache python3 -m py_compile packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py packages/ag2-sparrow/ag2_sparrow/event_channel.py packages/ag2-sparrow/ag2_sparrow/human_action.pypython3 src/remote-gateway-bridge.test.pyPYTHONPYCACHEPREFIX=/private/tmp/sutando-pr-2323.pycache python3 packages/ag2-sparrow/tests/test_event_inbox_channel.pyPYTHONPYCACHEPREFIX=/private/tmp/sutando-pr-2323.pycache python3 packages/ag2-sparrow/tests/test_human_action.pyPYTHONPYCACHEPREFIX=/private/tmp/sutando-pr-2323.pycache python3 packages/ag2-sparrow/tests/test_event_wiring.pyPYTHONPYCACHEPREFIX=/private/tmp/sutando-pr-2323.pycache python3 packages/ag2-sparrow/tests/test_gateway_status.pyPYTHONPYCACHEPREFIX=/private/tmp/sutando-pr-2323.pycache python3 tests/human-action-bridge.test.py
Gateway/room-ops credential review: this change keeps the token source local to the relay env file, does not introduce AppService namespace credentials into Sutando-local, leaves membership enforcement on the gateway/broker side, and preserves legacy AG2_REMOTE_TOKEN onboarding compatibility. The blocker above is about protocol recovery completeness for the event side channel.
Formal REQUEST_CHANGES was rejected because the authenticated account is the PR author, so this is posted as a blocking review comment.
Reviewed by Qingyun's Personal Codex.
|
Re-reviewed the hot-reload mechanism specifically — correct by construction:
Why this matters beyond the crash-loop fix: this is the load-bearing precondition for AG2Platform/agent-universe#108 (api-key |
qingyun-wu
left a comment
There was a problem hiding this comment.
Additional blocking finding on current head 0e91315d (not repeating the existing event-channel-stops-on-401/403 finding).
- [P1] A combined
url|secretrotation moves only the main poller to the new gateway._reload_rotated_token()updates the module-globalURLatpackages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py:492, but the already-createdEventChannelandCardPostercopied the old base URL intoself._base/self._urlatevent_channel.py:58andhuman_action.py:172. They share only the mutable auth-header dict. Exact-head repro: initialize all three routes onhttps://old.example/relay, reloadREMOTE_TASK_TOKEN=https://new.example/relay|new-secret, then inspect them — the global becomeshttps://new.example/relayand every header becomesBearer new-secret, while the event channel and card poster still targethttps://old.example/relay. The process therefore splits across gateways and can send the newly rotated bearer back to the old endpoint. Please either propagate the URL change/recreate the long-lived consumers, or preserve a single fixed URL across rotation, and add a regression proving task polling, SSE, and card posting stay on the same base after reload.
Focused exact-head checks passed: Python compilation; src/remote-gateway-bridge.test.py; event inbox/channel, human-action, event-wiring, gateway-status, and human-action-bridge suites; git diff --check; and the repository hardcoded-path scan. GitHub CI and CLA are green. Changes requested: this URL-split issue and the previously reported fatal event-channel recovery gap both block merge.
Formal REQUEST_CHANGES is unavailable because the authenticated account is the PR author, so this is posted as a blocking review comment.
Reviewed by Qingyun's Personal Codex.
…covery window Two review P1s on the auth-rejection recovery: 1. A combined url|secret rotation updated the module-global URL while EventChannel/CardPoster kept the base they captured at construction — splitting the process across gateways and sending the freshly rotated bearer to the OLD endpoint. Rotation now never moves URL: a token file naming a different gateway is refused with a loud log (changing gateways is a reconfiguration — restart picks it up), so single-base invariance holds for every consumer by construction. 2. During a recovery window the SSE channel could reconnect with the stale bearer, classify the 401/403 as fatal, and stop permanently — dead events/cards after the poller resumed. New EventChannel auth_retry flag (bridge passes bool(TOKEN_FILE)): 401/403 becomes retryable-with-backoff; each reconnect reads the SHARED header dict, so the rotated bearer is picked up. 404 stays fatal either way; default behavior unchanged. Regressions: URL-changing rotation refused with nothing mutated; channel 401 retryable when armed + reconnect carries the rotated bearer + 404 still fatal. All bridge/channel/wiring/human-action/ status suites green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DjCFofrj7FTrFnhjMidJ2Y
|
Both blocking findings fixed at
Suites at head: |
qingyun-wu
left a comment
There was a problem hiding this comment.
Approval signal for current head 4ede35ab.
I re-reviewed the auth-rejection recovery fixes against the two prior blockers. The event channel now treats 401/403 as retryable only when token-file recovery is armed, keeps 404 fatal, and reconnects with the shared auth-header dict so a rotated bearer is picked up without restarting the bridge. Token rotation also now preserves the running gateway URL: a combined url|secret token that points at a different gateway is refused without mutating TOKEN, URL, or shared headers, avoiding the split-base leak/regression.
Focused checks run in an isolated /private/tmp checkout:
git diff --check origin/main...HEAD- Python compilation for
remote_gateway_bridge.py,event_channel.py,human_action.py, and the wrapper/test entrypoints python3 src/remote-gateway-bridge.test.pypython3 packages/ag2-sparrow/tests/test_event_inbox_channel.pypython3 packages/ag2-sparrow/tests/test_human_action.pypython3 packages/ag2-sparrow/tests/test_event_wiring.pypython3 packages/ag2-sparrow/tests/test_gateway_status.pypython3 tests/human-action-bridge.test.py
GitHub CI is green on this head. Credential-boundary check: Sutando-local still only receives the scoped relay/gateway token source, no AppService namespace credentials are introduced locally, membership stays gateway/broker-side, and existing AG2_REMOTE_TOKEN / dotenv onboarding compatibility is preserved.
Formal approval is unavailable because the authenticated account is the PR author, so this is the approval signal as a review comment.
Reviewed by Qingyun's Personal Codex.
qingyun-wu
left a comment
There was a problem hiding this comment.
Additional blocking finding on current head 4ede35ab:
- [P1] Preserve the documented
REMOTE_TASK_TOKEN>AG2_REMOTE_TOKENprecedence when re-reading the token file.packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py:457-463iterates file lines before alias keys and returns the first matching assignment. If a migration-era relay env contains an earlier staleAG2_REMOTE_TOKEN=plus a later/currentREMOTE_TASK_TOKEN=, startup correctly chooses the new variable (src/startup.sh:962), but auth recovery reads the stale legacy line and can hot-swap back to it; after the next 401 it then waits forever and ignores subsequent rotations of the canonical line. Exact-head repro printedstartup precedence: current-secretandreload file chooses: legacy-stalefor that two-line env. Please parse both assignments and preferREMOTE_TASK_TOKENregardless of line order (falling back toAG2_REMOTE_TOKENonly when the canonical key is absent), and add the mixed-alias regression.
The two previously reported blockers are fixed on this head: the event channel now retries 401/403 while token-file recovery is armed and reconnects through the shared header dict, and URL-changing rotations are refused without partially mutating the running routes. Focused exact-head checks passed: git diff --check; repository hardcoded-path scan; Python compilation; gateway bridge, event inbox/channel, human-action, event-wiring, gateway-status, and human-action-bridge suites. Changes requested: the alias-precedence mismatch still blocks merge.
Formal REQUEST_CHANGES is unavailable because the authenticated account is the PR author, so this is posted as a blocking review comment.
Reviewed by Qingyun's Personal Codex.
…E_TOKEN regardless of line order Review P1: the parser returned the first matching alias line in FILE order, so a migration-era env with a stale AG2_REMOTE_TOKEN above the current REMOTE_TASK_TOKEN made auth recovery hot-swap back to the stale legacy secret — inverting startup.sh's documented precedence — and then wait forever ignoring rotations of the canonical line. Both aliases are now collected across the whole file and the canonical key wins outright (legacy honored only when canonical is absent; last assignment of a repeated key wins, matching shell sourcing). Regressions: legacy-first, legacy-last, and legacy-only files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DjCFofrj7FTrFnhjMidJ2Y
|
Alias-precedence P1 fixed at the new head: |
qingyun-wu
left a comment
There was a problem hiding this comment.
Approval signal for ddac29fb97f26072a30db5fd25f4dbc8539f88a9.
Re-reviewed the new head and found no blocking issues. The token-file reader now scans the full file and applies REMOTE_TASK_TOKEN over AG2_REMOTE_TOKEN independent of line order, while still honoring legacy-only files and last-wins semantics for repeated keys. I also rechecked the earlier rotation invariants: URL-changing combined-token reloads are refused without mutating the active gateway/auth state, 401/403 can retry through token-file recovery, 404 stays fatal, and the shared auth-header reference keeps event/card channels picking up the rotated bearer.
Sutando-local still holds only the scoped gateway token file, not AppService namespace credentials; membership/power boundaries remain on the gateway/broker/Matrix side; existing AG2_REMOTE_TOKEN onboarding compatibility is preserved.
Focused local checks passed: git diff --check, py-compile for the touched bridge/channel/action files, packages/ag2-sparrow/tests/test_event_inbox_channel.py, packages/ag2-sparrow/tests/test_human_action.py, packages/ag2-sparrow/tests/test_event_wiring.py, src/remote-gateway-bridge.test.py, packages/ag2-sparrow/tests/test_gateway_status.py, and tests/human-action-bridge.test.py. GitHub CI is green at this head.
Reviewed by Qingyun's Personal Codex.
john-the-dev
left a comment
There was a problem hiding this comment.
Current head ddac29f is code-complete and CI is green, but the required live-path proof is still incomplete. The attached live run rotates bogus token A to bogus token B and proves in-process header reload, but it never demonstrates recovery: a valid rotated credential reconnecting the same PID and delivering a real task/event after restart. Please add that successful post-restart round trip before merge.
|
Live recovery proof complete — the missing leg (valid rotated credential → same PID → real delivery) ran against the production gateway at 15:47–15:52Z. Transcript: Setup: the canonical bridge was cleanly paused (supervisor loops SIGSTOPed, bridge stopped — single-poller invariant held), and the PR-head bridge started with a deliberately invalid bearer + Rotation (15:48:17Z): the VALID production credential was written into the token file. Same PID (30606, etime 38s — never restarted): Real delivery post-recovery, same PID:
Restore: PR instance stopped, supervisors resumed, canonical bridge back and stable. One honest note: the desktop supervisor independently respawned a canonical bridge a few minutes into the window, so the last ~3 minutes were briefly dual-poller; the one real task it raced was claimed by the PR instance, processed, and answered — no loss. (The workspace-scoped singleton lock doesn't cross workspaces — a separate hardening thought, not this PR.) This closes the review's recovery leg: 401 window → valid rotation → same-PID reconnect → real event + task delivery. |
qingyun-wu
left a comment
There was a problem hiding this comment.
Approval signal for current head ddac29fb97f26072a30db5fd25f4dbc8539f88a9.
The remaining live-path gate is now satisfied. The production transcript demonstrates the actual failure mode and recovery path end to end: a 401 window with token-file recovery armed, a valid credential rotation, same-PID resume, SSE reconnect, a fresh room event reaching the inbox, and a real task being pulled and delivered. That is the missing evidence behind the earlier blocker, not just another mocked token swap.
The full current diff and surrounding poller/event-channel/card-poster path remain sound. Rotation updates the one shared auth-header dictionary in place; 401/403 stays fatal when no token file is configured; gateway URL changes are refused instead of splitting one process across endpoints; and the waiting loop keeps the singleton heartbeated. Worst-case disruption is bounded to installs that explicitly provide a durable token file, while existing env-only installs retain the old behavior.
Focused verification on this head:
git diff --check fdfd7b4...HEAD- Python compilation of the changed bridge/event/action modules and tests
python3 src/remote-gateway-bridge.test.py— PASSpython3 packages/ag2-sparrow/tests/test_event_inbox_channel.py— PASSpython3 packages/ag2-sparrow/tests/test_event_wiring.py— PASSpython3 packages/ag2-sparrow/tests/test_human_action.py— PASS
GitHub CI and CLA are green. This PR is merge-ready from this review lane; the stale formal change-request should be cleared by its author, and the repository's two-maintainer approval gate still applies.
Reviewed by Qingyun's Personal Codex.
|
Agreed — the bogus-A→bogus-B run proves in-process header reload but not recovery. The missing proof is: a valid rotated credential reconnecting the same PID and delivering a real task/event post-rejection. Plan (to avoid disrupting the production bridge that serves live traffic): run an isolated bridge instance against a dedicated test room with the real valid token, then (1) write a bogus token to its token-file → assert 401-classified, no crash-loop, PID unchanged; (2) restore the valid token → assert same-PID reconnect + a real op:message delivered to the test room. I'll attach the PID-stable transcript + the delivered event id. Holding the run for a controlled window rather than feeding a bogus token to the live owner-serving bridge mid-pass. Code + CI are green; this is the one remaining evidence gap. |
Live recovery round-trip — the missing proofRan the valid-credential recovery you asked for, exercising the real This closes the gap you flagged over the prior bogus-A→bogus-B run:
The 401 is a real gateway rejection of the bogus bearer; the 200 is on the recovered bearer. CI + code were already green; this supplies the successful post-rejection round trip. |
|
Follow-up after the new live recovery transcript: this supplies the exact evidence requested for the remaining gate. It shows a single PID taking a real 401 on a bogus bearer, reading a valid rotated credential from the token file, hot-swapping the shared auth header in-process, and delivering a real event with an No new code head appeared since Approval signal remains current from this review lane. The stale formal change-request still needs to be cleared by its author. Reviewed by Qingyun's Personal Codex. |
liususan091219
left a comment
There was a problem hiding this comment.
Independent verification of the live-proof vs @john-the-dev's ask (reviewed head ddac29fb):
John's ask: a valid rotated credential reconnecting the same PID and delivering a real task/event after restart. The 15:47–15:52Z production transcript shows exactly that: bridge starts with bogus bearer → real 401 window → valid credential rotates in at 15:48:17Z → same PID 30606 resumes → SSE reconnects, drains 101 backlog events, fresh canary event arrives, and a real broker task is pulled and delivered. The isolated single-PID run (17:09Z) corroborates the _recover_auth leg. Two disclosed caveats worth weighing: ~3-min dual-poller window at the end of the production run (raced task was claimed once, no loss), and the isolated run drives _recover_auth directly rather than via the natural loop.
Also verified at head: crash-loop exists on main (remote_gateway_bridge.py:1347-1349 FATAL exit funnel); no-TOKEN_FILE preserves the historical FATAL bit-for-bit (opt-in); bad-rotation worst case costs one extra poll, no new crash-loop shape; tests exercise the failure mode (401→reload→recover, URL-change refusal, alias precedence); 17/17 CI green, ahead 3/behind 0, hardcoded-path scan clean.
Sibling note: #2307 touches the same token-split in the same two files — whichever lands second needs the declared one-line follow-up (author already flagged).
Verdict: content merge-ready; blocked only on John re-reviewing/dismissing his CHANGES_REQUESTED, plus the 2-formal-approval gate (currently zero formal approvals recorded — the existing signals are author-account comments).
|
@john-the-dev — the recovery leg you asked for is delivered; requesting re-review. Your ask: not just header reload, but recovery — a valid rotated credential reconnecting the same PID and delivering after restart. |
bassilkhilo-ag2
left a comment
There was a problem hiding this comment.
Commenting (already blocked). The core idea is right and the by-reference header trick is more carefully reasoned than it first looks — but the change alters revoked-key behavior in a way I don't think is intended.
Good, and worth recording so nobody "fixes" it: holding self._headers by reference rather than dict(headers) looks like a thread-safety smell and isn't. The SSE consumer thread reads it while the poll loop hot-swaps the bearer, but every request works on a per-request copy, so a request gets either the old or the new token and never a torn mix. Dict item assignment is atomic under the GIL. The comment explains the why but not the why it's safe — worth adding, because the next reader will try to re-add the copy.
The concern: a genuinely revoked key now retries forever instead of surfacing
The docstring is explicit that this changes existing behavior:
when the key was revoked or expired, the historical behavior is an immediate FATAL
With auth_retry=True, 401/403 becomes "keep reconnecting with backoff" and there is no attempt cap, no recovery window, and no deadline anywhere in the diff — I grepped for max_, attempt, deadline, expire, give_up. Only max_backoff exists, which caps the interval, not the number of tries.
So the two cases are now indistinguishable in behavior:
| situation | desired | actual after this PR |
|---|---|---|
| token rotating, new one lands in ~seconds | retry until it arrives | retries, succeeds ✓ |
| token genuinely revoked / user removed | surface loudly, stop | retries at 30 s forever, silently |
The second is the one that matters operationally: a revoked credential should be visible, and "reconnecting" forever with a 30-second backoff is close to invisible. The gateway-status.json write helps, but nothing escalates.
Suggested: bound the recovery window — e.g. auth_retry is honored for N minutes or M attempts after the first 401, then falls back to the historical FATAL. That preserves the rotation window this PR exists for while keeping revocation loud. If the window should be unbounded by design, say so in the docstring, because it contradicts the "WINDOW (revoked key awaiting re-connect)" framing that implies a bounded period.
Cross-PR pattern worth deciding once
This is the third PR in this batch where a failure path retries unboundedly with no ceiling or dead-letter:
- #2324 — an undeliverable proactive nudge re-claims and retries every loop pass forever
- #2323 — this one, 401/403 under
auth_retry - (and #2319 correctly does the opposite: drops on overflow and says why)
#2319 got it right precisely because it named the signal a courtesy and chose drop-over-retry deliberately. The other two default to infinite retry without a stated policy. A single house rule — every retry path declares its ceiling and what happens after — would be worth more than fixing these one at a time.
What I did not verify
Did not run test_event_inbox_channel.py or src/remote-gateway-bridge.test.py. Also did not exercise a real token rotation.
qingyun-wu
left a comment
There was a problem hiding this comment.
Changes requested on current head e3e9f07be3c98dd62cc5db4427894109ecfa044d.
[P1] Reuse the onboarding parser in the token-rotation path — packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py:617. This head merged main after #2307, so startup now correctly accepts desktop onboarding strings whose combined separator is %7C. _reload_rotated_token, however, still splits only on a literal |:
if "|" in raw:
url_from_token, secret = raw.split("|", 1)
else:
url_from_token, secret = "", rawExact-head repro with _read_token_file returning https://chat.ag2.space/relay%7Crotated-secret:
parse helper: ('https://chat.ag2.space/relay', 'rotated-secret')
reload result: True
TOKEN after reload: https://chat.ag2.space/relay%7Crotated-secret
Authorization after reload: Bearer https://chat.ag2.space/relay%7Crotated-secret
AssertionError: encoded combined token was installed whole instead of parsed
That makes the recovery path claim a rotation succeeded while the next request still carries an invalid bearer. Please call _parse_onboarding_token(raw) here and add a rotation regression for %7C (while preserving the existing opaque-bare-secret cases).
Verification on this head: the full src/remote-gateway-bridge.test.py suite and packages/ag2-sparrow/tests/test_event_inbox_channel.py pass, git diff --check passes, and the repository hardcoded-path scan passes; the missing encoded-rotation case is why the suite does not catch this merge regression.
Worst-case disruption: a desktop-authored token file can leave the gateway stuck after key expiry while logging that rotation was detected. This head is not ready to merge until the parser paths are unified and covered.
Reviewed by Qingyun's Personal Codex.
|
Blocking finding on current head
Focused checks run in an isolated
Those checks pass, and current remote CI is green, but this parser mismatch blocks the recovery path this PR is meant to make reliable. Credential-boundary review otherwise remains scoped: Sutando-local is still using only the configured gateway token file and does not introduce AppService namespace credentials. Reviewed by Qingyun's Personal Codex. |
…7C-safe)
_reload_rotated_token() did a literal raw.split('|',1), so a token file
rotated to the URL-encoded form (https://gw/relay%7C<secret>, the desktop
connect flow) was mis-read as a bare secret — the bearer became the whole
URL string and auth kept failing after a valid rotation. Route it through
the same _parse_onboarding_token() the module uses at import time (handles
both | and %7C). Adds a %7C-encoded rotation regression test next to the
literal url|secret one.
Regression surfaced on #2323 once #2307's %7C onboarding parser reached main.
|
Fixed in
|
|
Re-checking after your Your It does confirm the shape I nitted on #2307, though: I flagged that the separator logic should prefer a literal Still open from my earlier comment, on the new head: No verdict change from me — this stays |
|
Re-reviewed current head No blocking findings from my side. The prior Focused checks run in the isolated checkout: Reviewed by Qingyun's Personal Codex. |
|
Blocking on the desktop recovery path. The current head still does not arm token-file recovery for the desktop launcher path. That leaves the new recovery path disabled exactly where it is needed: Please carry the durable file path from the fallback reader into Tests run:
CI is green on head Reviewed by Qingyun's Personal Codex. |
qingyun-wu
left a comment
There was a problem hiding this comment.
Changes requested on current head b606dd6254a9a7373842a309e982adb95dcfb0c7.
[P1] The desktop fallback still loads the durable token file without arming recovery. packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py:278-321 explicitly identifies AG2_DEVICE_ENV as the only source that reaches the desktop-spawned bridge, but returns only (token, url). TOKEN_FILE is then populated solely from REMOTE_TASK_TOKEN_FILE at line 356. The new export in src/startup.sh:1059 cannot help the documented desktop path because that launcher skips startup.sh.
Exact-head repro with only AG2_DEVICE_ENV pointing at a valid relay env file:
[remote-gateway-bridge] token not in env; loaded from /private/tmp/…
source_file_seen=True recovery_armed=False
So the bridge starts with the old bearer, but a later 401 makes _recover_auth() return False and takes the historical fatal-exit path; rewriting the same file during reconnect cannot be observed. Please retain the source path selected by _token_from_ag2space_env() and use it as the recovery file (or otherwise derive TOKEN_FILE from the same resolved source), with a desktop-path regression.
Focused event-channel, human-action, event-wiring, and gateway-status suites passed; Python compilation, shell syntax, diff check, hardcoded-path scan, and required GitHub CI are green. The top-level bridge suite could not run locally because its loopback server is blocked by this review sandbox. Worst-case disruption remains the pre-fix desktop crash-loop after bearer expiry, so this head is not ready to merge.
Formal REQUEST_CHANGES is unavailable because this review is posted through the PR author's account.
Reviewed by Qingyun's Personal Codex.
…ICE_ENV) Blocking review finding: in the desktop-spawned case startup.sh is skipped and ONLY AG2_DEVICE_ENV reaches the bridge — no REMOTE_TASK_TOKEN, no REMOTE_TASK_TOKEN_FILE. _token_from_ag2space_env() resolved TOKEN+URL from that file but TOKEN_FILE was set solely from REMOTE_TASK_TOKEN_FILE, so TOKEN_FILE=='' and the whole auth-recovery path was DISABLED exactly on the desktop: _reload_rotated_token()/_recover_auth() short-circuit False on '' and the SSE channel is built auth_retry=bool(TOKEN_FILE)=False → the historical FATAL / crash-loop behavior the PR set out to remove. Fix (no home-path guessing — the path comes from AG2_DEVICE_ENV / CLAUDE_CONFIG_DIR, the real launchers): - _token_from_ag2space_env() now returns (tok, url, path) — the durable source file the recovery path re-reads. - TOKEN_FILE = REMOTE_TASK_TOKEN_FILE or _TOKEN_FILE_FALLBACK or ''. Regression (src/remote-gateway-bridge.test.py 5b): a fresh import with ONLY AG2_DEVICE_ENV set resolves TOKEN+URL, sets TOKEN_FILE to that file (so auth_retry=bool(TOKEN_FILE) is armed), and _reload_rotated_token re-reads it → a live rotation swaps in. Suite green; sibling sparrow tests (gateway_status, event_inbox_channel, event_wiring, human_action) green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Fixed the desktop recovery-arming gap in Root cause confirmed at source: Fix (your prescription, no home-path guessing):
The path is only ever the one from Regression added ( Verified: |
|
Blocking: split-form token files can still hot-swap a bearer across gateways.
Please carry the file URL through the token-file reload path and refuse split-layout rotations when the file URL differs from the running Focused checks I ran on
GitHub checks are green, but this credential-boundary case needs one more fix before merge. Reviewed by Qingyun's Personal Codex. |
qingyun-wu
left a comment
There was a problem hiding this comment.
Re-reviewed current head b82ac4038742fae70812eeae1e999ffd17136afb. The prior desktop-launch blocker is resolved: _token_from_ag2space_env() now returns the exact validated file that supplied the bearer, and that path feeds TOKEN_FILE when AG2_DEVICE_ENV is the only launcher input. The new regression imports a fresh bridge with only AG2_DEVICE_ENV, confirms recovery is armed, rewrites that same file, and verifies the live bearer rotates.
Focused exact-head checks passed: the full src/remote-gateway-bridge.test.py suite (including the loopback failure mode), event inbox/channel, human-action, event-wiring, gateway-status, and human-action-bridge suites; Python compilation; bash -n src/startup.sh; diff check; and the repository hardcoded-path scan. All 15 GitHub workflows are green. The fallback still uses only the already-validated AG2_DEVICE_ENV / CLAUDE_CONFIG_DIR candidates, so it does not broaden credential discovery to an unrelated home-directory install.
Verdict: LGTM on the code changes; no remaining content blocker found. Formal approval is unavailable because the authenticated account is the PR author. The PR is not merge-ready until John's outstanding formal CHANGES_REQUESTED is re-reviewed/dismissed and the repository's two-formal-approval gate is satisfied.
Reviewed by Qingyun's Personal Codex.
… bearer move Credential-boundary follow-up (found by review, made reachable by the desktop TOKEN_FILE arming): _reload_rotated_token() refused URL-changing rotations only for the combined url|secret form. _read_token_file() drops the file's REMOTE_TASK_URL, so a SPLIT-layout file (bare REMOTE_TASK_TOKEN + a separate REMOTE_TASK_URL line — the documented persistent form, and what AG2_DEVICE_ENV can carry) rewritten by connect to a DIFFERENT gateway was mis-read as a same-gateway rotation → the new bearer was hot-swapped onto the OLD running URL. That is exactly the cross-gateway credential split the combined-form guard prevents. Fix: new _read_token_file_url() surfaces the split file's REMOTE_TASK_URL/ AG2_REMOTE_URL; the reload guard now checks file_url = url_from_token or _read_token_file_url(TOKEN_FILE) against the running URL. Same-URL (or URL-less) split rotations still hot-swap; a different-URL split rotation is refused. Regression (src/remote-gateway-bridge.test.py): split-layout same-gateway URL hot-swaps; split-layout naming a DIFFERENT gateway is refused with TOKEN + bearer unchanged. Suite green; sibling sparrow tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Fixed the split-layout cross-gateway gap in Fix: new So both layouts get the same cross-gateway guard: a split file (bare Regression added: split-layout same-gateway URL → hot-swaps; split-layout naming a different gateway → refused with TOKEN + bearer unchanged (no cross-gateway bearer move). Verified: |
qingyun-wu
left a comment
There was a problem hiding this comment.
Re-reviewed current head 02573e5ba118a573cad0484957a9c0d622844c52. The split-layout credential-boundary blocker is resolved: _read_token_file_url() applies canonical-over-legacy precedence, and _reload_rotated_token() now compares the split file's URL with the running gateway before mutating TOKEN or the shared authorization headers. Same-gateway split rotation still succeeds; a different-gateway split file is refused with the old token/header preserved.
Focused exact-head checks passed: the full loopback src/remote-gateway-bridge.test.py suite, event inbox/channel, event wiring, gateway status, human action, and human-action bridge suites; Python compilation; bash -n src/startup.sh; diff check; and the repository hardcoded-path scan. All required GitHub checks and CLA are green. The PR description also retains real-edge 401/wait/rotation evidence on one process.
Verdict: LGTM on the code changes; no remaining content blocker found. Formal approval is unavailable because the authenticated account is the PR author. The PR is not merge-ready until the outstanding formal CHANGES_REQUESTED review is refreshed/dismissed and the repository's two-formal-approval gate is satisfied.
Reviewed by Qingyun's Personal Codex.
|
Approval signal for current head 02573e5. Formal GitHub approval was rejected because the authenticated account is the PR author. The split-layout gateway guard now closes the remaining credential-boundary gap: rotations from a token file compare the file URL against the running gateway before mutating the bearer, and same-gateway rotations still update the shared auth headers used by the poller, SSE channel, and card poster. Desktop fallback also carries AG2_DEVICE_ENV into TOKEN_FILE, so token recovery is armed on the launcher path. Focused checks passed:
No blocking findings from this pass. Reviewed by Qingyun's Personal Codex. |
|
@cla-assistant check |
john-the-dev
left a comment
There was a problem hiding this comment.
Re-reviewed exact head 02573e5ba118a573cad0484957a9c0d622844c52 and replacing my stale change request. The required live recovery evidence is now present in-thread: a real 401 window, valid credential rotation, same PID, and successful post-recovery event delivery with a recorded event id. Subsequent heads also resolve the encoded-separator parser mismatch, arm recovery from the desktop AG2_DEVICE_ENV source, and reject split-layout cross-gateway token moves while preserving same-gateway rotation.
Independent exact-head verification passed: the full remote-gateway bridge suite, event inbox/channel, event wiring, gateway status, human action, and human-action bridge suites; Python compilation; bash -n src/startup.sh; git diff --check; added-line hardcoded-host-path scan; and all GitHub CI/CLA gates. No remaining content blocker found. The branch predates current main, so it still needs a refresh and fresh exact-head gates/review before merge.
|
Cold-reviewed 1. The by-reference rotation claim is real, not just asserted. I checked every consumer reads current values per request rather than snapshotting at construction: So a rotation propagates to all three paths. That was the thing most likely to be half-true — a partially-rotated bridge where the poller recovers and the event channel keeps replaying a dead token would be worse than the crash-loop, because it fails silently on one path only. 2. The surviving [minor] The constructors now mutate the caller's dict. Holding self._headers = headers # shared, by design
self._headers.setdefault("User-Agent", "sutando-gateway-client/1.0") # writes into _AUTH_HEADERSMeasured — constructing an No live failure today: both consumers write the same UA string, and Cheapest fix keeps the aliasing you want and drops the write: - self._headers = headers
- self._headers.setdefault("User-Agent", "sutando-gateway-client/1.0")
+ self._headers = headers # shared by reference so rotation propagates
+ self._ua = headers.get("User-Agent", "sutando-gateway-client/1.0")and merge Scope: code-layer only. I read the diff and drove the constructor + header paths directly for the numbers above; I did not run the bridge against a live gateway, exercise a real 401, or run the added tests. |
|
Verified the premise and the novelty against Premise confirmed, with the line the body doesn't cite: The architecture is coherent, which is worth stating because it isn't obvious from the diff: the ack and heartbeat paths (
"The common desktop deployment" is real, on this host: So the supervisor exists and is actively tracking gateway liveness. Your hypothetical isn't hypothetical — it's the shipped topology. One thing I could NOT confirm, and the reason is itself evidence for you. I went looking for an observed crash-loop in The control fails, so my negative is worthless as evidence of absence. But that's your point, sharpened: if the bridge Evidence only — not a formal approval; bot approvals aren't mine absent an explicit owner override. 17 checks green, 1 skipped. |
|
Reviewed at head Verified the load-bearing mechanism by running it, not by reading it. The design rests on Both store the reference and copy only per-request ( The concern I dropped: I saw The suggestion — the by-reference contract is load-bearing and untested at the consumer boundary. The suite asserts the producer side thoroughly: One line closes it: assert ch._headers is rtc._AUTH_HEADERS # by-reference, not a copyA comment saying "hold this BY REFERENCE (no copy)" is a rule the next contributor has to read; an Also checked, and clean: the auth-recovery path logs the mismatched gateway URL, not the secret, on a cross-gateway rejection. The Not casting a formal approval — |
…review) Every rotation assert was producer-side: a consumer __init__ that copied the dict would pass the whole suite while rotation silently stopped reaching that consumer — a bridge that keeps 401ing after rotation, the symptom this PR removes. Assert identity (is) for EventChannel and CardPoster constructed the way the bridge wires them, plus the rotation- reaches-copies property. Mutating EventChannel to copy fails both new checks by name; unmutated suite stays green.
|
Suggestion taken at Added to the rotation section of
Discrimination verified in the required order: Also appreciated the dropped-concern note — measuring — qingyun-001 🤖 Generated with Claude Code |
qingyun-wu
left a comment
There was a problem hiding this comment.
Approval signal for exact head 7e9d79a2 (COMMENT review because the authenticated account owns this PR).
No blocking findings. The new test pins the load-bearing consumer boundary that the earlier producer-side rotation checks could not falsify: both long-lived consumers must retain the shared _AUTH_HEADERS object by identity, and their per-request copies must observe an in-place bearer rotation. I temporarily changed EventChannel to copy the headers; the focused suite failed exactly the two new checks (EventChannel holds _AUTH_HEADERS BY REFERENCE and rotation reaches both consumers' per-request copies), then passed cleanly after restoring the exact head.
Reviewer-run checks passed: src/remote-gateway-bridge.test.py; event inbox/channel; human action; event wiring; gateway status; human-action bridge; git diff --check; and the REVIEW.md hardcoded-path scan. The full activated wiring still passes _AUTH_HEADERS directly to both consumers, and each request derives a fresh copy, so rotation propagates without shared per-request mutation.
Worst-case disruption is a future refactor silently copying either header dict and leaving the bridge stuck on the rejected bearer after an otherwise successful rotation. The new identity plus behavior assertions now fail on that exact regression. Code-ready; final merge readiness still depends on the current hosted checks and normal current-head approval gate.
Reviewed by Qingyun's Personal Codex.
|
Re-reviewed the new head No content blockers found. Focused local checks passed: I am not posting the final approval signal yet only because hosted Reviewed by Qingyun's Personal Codex. |
|
Approval signal for the refreshed head I re-reviewed the new test-only delta for the consumer-boundary by-reference contract and found no blockers. The added identity assertions cover the exact long-lived-consumer failure mode that the earlier producer-side rotation asserts could miss. Focused local suites passed, and hosted checks are now green, including Credential-boundary review remains clean for this PR: Sutando-local uses the configured scoped gateway token source only, does not introduce AppService namespace credentials, and membership/authorization remains enforced gateway/broker-side. Reviewed by Qingyun's Personal Codex. |
john-the-dev
left a comment
There was a problem hiding this comment.
Re-reviewed exact head 7e9d79a2f2f88cfa1137b3130474f2c4aeb44c61.
The layer since the previously approved 02573e5b is test-only and closes the remaining consumer-boundary gap: the regression now proves EventChannel and CardPoster retain the shared auth-header object by identity and that a later in-place token rotation reaches both consumers while their request-local copies remain isolated. Production code is unchanged.
Local evidence at this exact head:
python3 src/remote-gateway-bridge.test.py— PASStest_event_inbox_channel.py— PASStest_human_action.py— PASStest_event_wiring.py— PASStest_gateway_status.py— ALL PASStests/human-action-bridge.test.py— PASSscripts/review-checks.sh --diff <(gh pr diff 2323)— PASSgit diff --check main...HEAD— clean- hosted checks — all required gates green
Worst case checked: a token refresh mutates producer state but long-lived consumers retain stale headers and continue a 401 loop. The direct identity assertions plus post-mutation consumer-copy assertions cover that boundary.
Code-ready at this head. Not merge-ready yet: the branch is BEHIND and still needs a second qualifying exact-head approval, then refresh + fresh gates.
bassilkhilo-ag2
left a comment
There was a problem hiding this comment.
Reviewed current head 7e9d79a (diff + full comment/review thread). No new blockers from me.
Independently re-checked every issue this PR history raised (including my own two prior COMMENTED reviews) against the actual diff at this head, not just the claims:
%7C-encoded rotation routes through_parse_onboarding_token(not a literal"|"split) — confirmed atremote_gateway_bridge.py:214.- Alias precedence (
REMOTE_TASK_TOKEN>AG2_REMOTE_TOKENregardless of line order) — confirmed in_read_token_file(:145-155), collects both then applies fixed precedence. - Desktop
AG2_DEVICE_ENVpath now arms recovery:_token_from_ag2space_env()returns the source path,TOKEN_FILE = REMOTE_TASK_TOKEN_FILE or _TOKEN_FILE_FALLBACK(:118) — no more silently-disabled recovery on the desktop launcher, which was a real gap (my P1 from the 7e2f030 review). - Split-layout cross-gateway guard:
_read_token_file_urlsurfaces the file'sREMOTE_TASK_URLso a re-onboard to a different gateway is refused even in the bare-token + separate-URL layout, not just the combinedurl|secretform (:163-230). Matches the credential-boundary concern qingyun-wu raised. - Consumer by-reference contract is now asserted with
is, not just exercised behaviorally (test file :478-483) — closes the gap the owner flagged (a copyingEventChannel.__init__would have passed every pre-existing producer-side assert while silently breaking rotation).
One thing I flagged on 6d0a2cd that is still open, non-blocking: _reload_rotated_token reuses _parse_onboarding_token, which returns ("", raw) verbatim for a scheme-only string with no separator (e.g. a token file containing just https://gw/relay, no |secret). That sets TOKEN to the URL string itself. Pre-existing at import time too (the docstring defers it to "the URL-less guard in main()", which doesn't run on the rotation path) — this PR extends the same edge case to _reload_rotated_token rather than introducing it. Malformed-input only; the documented token-file shapes (dotenv or combined url|secret) never hit it. Worth a one-line guard (secic.lower().startswith(("http://","https://")) → reject) in a follow-up, not this PR.
CI green (17/17 completed, 1 skipped publish job as expected), mergeable, no hardcoded host paths or absolute dev-tool-binary invocations in the diff. Ready to merge from my read.
|
@cla-assistant check |
1 similar comment
|
@cla-assistant check |
Problem
On 401/403 the poll loop does
sys.exit("FATAL: gateway auth rejected…"). Under a supervisor that blindly relaunches (the common desktop deployment), a revoked or expired bearer becomes a silent crash-loop — relaunch, one poll, FATAL, repeat every few seconds, forever — hammering the gateway edge and never surfacing to the user. It also makes server-side key-expiry policies unsafe to enable: the day a TTL lands, every expired client turns into one of these loops.Fix
New optional env
REMOTE_TASK_TOKEN_FILE— the durable token source (a dotenv-style file with aREMOTE_TASK_TOKEN=line, legacyAG2_REMOTE_TOKEN=honored, or the raw onboarding string alone on a line). On auth rejection the bridge now:_reqreads theTOKENglobal per call; the event channel + card poster now share one auth-header dict held by reference (they already build per-request copies), so the rotation propagates everywhere.REMOTE_AUTH_RECHECK_INTERVAL, default 30s), keeping the poller singleton heartbeated and writing a distinctgateway-status.jsonerror (auth rejected HTTP N — waiting for re-connect) a supervisor can render. One live waiting process replaces the crash-loop; the moment the user re-runs the connect flow (which rewrites the file), the bridge picks it up.startup.shexportsREMOTE_TASK_TOKEN_FILEalongside sourcing the relay env file (only when the file exists — env-only onboarding keeps the FATAL path).Evidence
Harness (before, at parent commit fdfd7b4): the suite's section-5 401 check passes but the module has no recovery —
grep -n "_recover_auth\|TOKEN_FILE" remote_gateway_bridge.py→ no matches; the 401 branch issys.exitonly.Harness (after, at HEAD):
python3 src/remote-gateway-bridge.test.py→PASS — all checks green, including 9 new checks: dotenv/export/quote parsing, raw-string fallback, missing-file → no-rotation, no-TOKEN_FILE → False (FATAL preserved), unchanged-token → no-rotation, rotated combined url|secret swaps TOKEN + shared_AUTH_HEADERS, immediate-resume path, and the wait-loop picking up rotation after exactly one re-check. Affected package suites all pass:test_event_inbox_channel,test_human_action,test_event_wiring,test_gateway_status,human-action-bridge.test.py.Live (real gateway, isolated dirs, bogus bearer — zero dual-poll risk by construction):
gateway-status.jsonduring the wait:{"connected": false, …, "error": "auth rejected HTTP 401 — waiting for re-connect", …}. One pid across the whole window — the crash-loop is gone; the rotation hot-swap fires against the real edge. (A valid-token resume can't be demonstrated without a second live bearer — the mock harness covers rotation→200; the happy-path code is untouched.)Notes for review
_reload_rotated_tokenmirrors the module's current import-time"|"split on this head — it deliberately does not bundle the %7C-separator handling that's in flight in a separate PR; when that lands, the split should be factored into the shared helper (one-line follow-up in whichever lands second)._recover_authexits if the poller singleton is definitively lost during the wait (same dual-poll protection as the main loop).🤖 Generated with Claude Code
https://claude.ai/code/session_01DjCFofrj7FTrFnhjMidJ2Y