Repository navigation
fix(gateway): resolve access map from active config - #2340
Conversation
john-the-dev
left a comment
There was a problem hiding this comment.
Blocking on current head aca241f:\n\n- packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py, _load_tier_map(): when _ag2space_access_path() returns an empty string after configuration is removed, os.path.getmtime("") raises and the OSError branch returns _TIER_MAP_CACHE["map"]. If this process previously loaded a tier map, the supposedly disabled configuration keeps applying that stale trust map. This contradicts the PR body/added test claim that no explicit launcher config disables the map. Please clear/return {} when path is empty and add a regression that first loads a map, removes both config pointers, then verifies _tier_for() no longer uses it.\n\n- Live evidence required after that fix: this changes the gateway trust-classification path. Please include one real desktop/bare-launch restart using AG2_DEVICE_ENV without CLAUDE_CONFIG_DIR, then show the selected access.json log line and one sender classified from that map. Harness-only proof is not enough for this delivery/startup path.\n\nEvidence reviewed: commit aca241f, full diff, PR validation description, and current CLA state. CI workflows beyond CLA have not appeared yet, so they are not verified.
|
@john-the-dev Both requested changes are addressed on current head
|
john-the-dev
left a comment
There was a problem hiding this comment.
Current-head re-review on c84a78b.
The stale-tier-cache blocker is resolved: _load_tier_map() now treats removal of both explicit launcher pointers as a deliberate disable, clears the cached path/mtime/map, and returns the local tier. The new regression first loads a mapped sender, removes both pointers, and verifies the stale trust decision is not retained.
I also verified the requested live-path evidence in the PR body: the real bare ag2-sparrow entrypoint was started twice with only AG2_DEVICE_ENV; both starts logged the sibling access.json and serialized the mapped sender as access_tier: other before and after restart.
Focused verification on this head passed locally: access-path 7/7, per-sender tier 13/13, both remote-gateway suites, Python compilation, git diff --check, and scripts/review-checks.sh with the hardcoded-path scan clean. No remaining blocker from my prior review. The branch will still need a normal refresh/check rerun after today’s #2280 merge before merging.
Reviewed by John’s Codex.
|
Refreshed this approved branch onto post-#2280 Fresh rerun: access-path 7/7, per-sender tier 13/13, both remote-gateway suites, Python compilation, Ruff 0.15.22, |
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 79%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changesNo lines with coverage information in this diff. |
john-the-dev
left a comment
There was a problem hiding this comment.
New blocker on exact head a09ea40 after tracing the path-switch failure mode. In _load_tier_map() lines 469-473, an OSError always returns the cached map even when the newly resolved path differs from the cached path. If install/config A loaded a map, then AG2_DEVICE_ENV switches to config B whose access.json is missing or unreadable, senders continue receiving A’s tier decisions. The empty-path fix does not cover this non-empty foreign-path case.
Please clear/fail to LOCAL_TIER when path != cache[path] and the new path cannot be read, while retaining last-known-good only for read failures on the SAME configured path. Add a regression that loads map A, switches to a different missing/unreadable path B, and verifies A is not reused. This is a trust-classification boundary, so my prior approval is withdrawn until that cross-config cache case is fixed and rerun.
|
Addressed the requested cache-boundary fix in e06ae17. A switch from config path A to a missing or unreadable path B now clears the cached map and falls back to LOCAL_TIER; last-known-good retention remains scoped to failures on the same configured path. Added regression coverage for A -> missing B, B becoming readable, and same-path B removal. Validation: gateway access-path tests (10/10), per-sender tier tests (13/13), both remote-gateway bridge suites, py_compile, Ruff 0.15.22, and diff-check all pass. The branch is also synced with current main. |
liususan091219
left a comment
There was a problem hiding this comment.
APPROVE on exact head e314e665 — re-reviewed the tier-cache isolation fix (e06ae17): same-path transient faults keep last-known-good (map floor never drops) but a config-PATH SWITCH with unreadable target now CLEARS instead of carrying trust maps across installs — the cross-install contamination hole closed, distinction verified in diff + the suite's new same-path/clear cases; 10/10 access-path checks green at this head, CI green. Original verification (at a09ea40): gateway access-map resolution verified: AG2_DEVICE_ENV-sibling first, CLAUDE_CONFIG_DIR second, deliberately NO ~/.claude guess (wrong-install trust-map hazard — same reviewed pattern as #2331's token fallback); key safety properties confirmed in code: map only DOWN-tiers named senders (no escalation path for unknowns, owner stays owner by absence), absent config = disabled, path changes logged (closes the no-diagnostic complaint), hot mtime reload; 7/7 access-path checks run green by reviewer at head incl. config-removal-clears-map.
|
Refreshed the branch onto current main (c71a637) after the 24-hour review grace window; new head is f1586f6. The 13 main-only commits did not overlap the PR files, and the PR diff remains limited to the gateway implementation and access-path regression. Fresh validation passes: access-path 10/10, per-sender tier 13/13, both remote-gateway suites, Python compilation, Ruff 0.15.22, and git diff --check. |
john-the-dev
left a comment
There was a problem hiding this comment.
Re-reviewed exact head f1586f69c096830101003658bcf0c88072976707.
My prior cross-config trust-cache blocker is resolved. _load_tier_map() now distinguishes a foreign path from a transient fault on the same configured path: switching to a missing/unreadable path clears the previous install’s map and returns local tier, while a same-path transient failure retains only that path’s last-known-good map. The regression covers load A → missing B → empty, B appearing → load B, same-path disappearance → retain B, and removing both config pointers → clear.
Independent exact-head verification passed: access-path suite 10/10, per-sender tier suite 13/13, both remote-gateway bridge suites, Python compilation, git diff --check origin/main...HEAD, and the added-line hardcoded-host-path scan. All required GitHub CI and CLA checks are green. The PR body’s real bare-launch restart proof covers the unchanged startup path: AG2_DEVICE_ENV without CLAUDE_CONFIG_DIR selected/logged the sibling access map and serialized the mapped sender as other before and after restart.
No blocker remains from my review. Approved.
john-the-dev
left a comment
There was a problem hiding this comment.
Re-reviewed refreshed exact head 09b590822fbc253b71424b4ae1c7edb14c967bf6. This head is based on current main 3e1dd21e, and both PR-owned blobs are byte-identical to the previously verified f1586f69 head (production ac3ba1ee, focused test 1c233854). Independent detached exact-head verification passes access-path 10/10, per-sender tier 13/13, both remote-gateway bridge suites, Python compilation, diff hygiene, repository review checks, and the added-line hardcoded-host-path scan. Every GitHub CI/coverage/lint job is green and GitHub reports the head mergeable. The cross-config trust-cache fix remains intact: a missing foreign path clears the prior install's map, while same-path transient failure retains only that path's last-known-good map; explicit config removal clears it. The prior real bare-launch restart proof still applies because the startup-path blobs are unchanged. No blocker remains from my lane. One more qualifying formal approval must target this exact refreshed head before merge.
|
Refreshed this approved branch after the 24-hour review grace window onto current The merge was clean. Fresh focused validation passes: access-path 10/10, per-sender tier 13/13, all remote-gateway bridge/attachment/interaction/singleton suites, Python compilation, diff hygiene, and repository review checks. Ruff is not installed in this local checkout, so I am leaving that claim to the required GitHub Ruff gate now running. |
|
This head has two qualifying exact-head approvals, green CI/CLA, and concrete bare-process live restart evidence. Required-up-to-date protection is the only merge blocker. I attempted GitHub's update-branch API, but the repository token cannot update this fork ( |
|
Refreshed the branch onto current main (ecfe08a) as requested; new head is e86f591 and the branch is 0 behind. The merge was clean and did not change either PR-owned blob. Fresh validation passes: access-path 10/10, per-sender tier 13/13, all remote-gateway bridge/file-attach/interaction/singleton suites, Python compilation, diff hygiene, and repository review checks. @john-the-dev fresh exact-head review is requested. |
|
@cla-assistant check |
bassilkhilo-ag2
left a comment
There was a problem hiding this comment.
Independently verified: read the full diff in remote_gateway_bridge.py end-to-end (resolver, cache, _tier_for) — no other callers of _ag2space_access_path/_TIER_MAP_CACHE exist outside this file. Confirmed the bug on pre-fix main by reading the old resolver (CLAUDE_CONFIG_DIR or blind ~/.claude fallback, ignoring AG2_DEVICE_ENV).
Ran the actual regression proof both ways: checked out the PR's new tests/gateway-access-path-resolution.test.py against pre-fix main's remote_gateway_bridge.py — 8/10 checks FAIL (wrong path resolved, stale-home guess not blocked, cache doesn't invalidate on path switch). Same test against the PR head — 10/10 pass. Also ran gateway-per-sender-tier.test.py (13/13) and both bridge suites — all green, matching the PR's own validation log.
No hardcoded paths, fail-closed when neither AG2_DEVICE_ENV nor CLAUDE_CONFIG_DIR is set (consistent with src/startup.sh always exporting CLAUDE_CONFIG_DIR, so this is a narrow edge case, not a default-path regression for normal Sutando boots). CI is green and this matches the existing exact-head APPROVED decision.
Ready to merge.
|
@cla-assistant check |
Head branch was pushed to by a user without write access
|
Refreshed the conflicting branch onto current |
|
Heads-up, not a review: #2340 and #2591 conflict with each other, so whichever of the two merges second will need a rebase. Both are CLEAN + APPROVED right now, which is what makes this easy to miss — GitHub computes Measured against Scope, so this doesn't read as bigger than it is: I checked each of these against every other merge-ready PR — there are 21 — and this pair is the only conflict among them. The remaining 19 merge in any order (28 orderings tested, all clean), and 19 + either one of you is clean. So nothing here blocks either PR on its own; it's purely "second one rebases." No action requested from me, and no opinion on which should land first — that's the maintainers' call. |
|
Cold review at head Verified by running, not reading — worktree pinned to a throwaway workspace first (asserted the resolution before executing, since these tests write config trees): The regression covers the direction that matters. For a trust map the dangerous failure is fail-OPEN, and half your assertions are exactly that case:
One suggestion, and it's that the body undersells the code. The summary says "cache by path and mtime so an active-config switch reloads even when mtimes match." The implementation is materially stronger than that sentence: ident = (st.st_mtime_ns, st.st_size, st.st_ino) # not float mtime
...
and not _has_above_local(_TIER_MAP_CACHE["map"]) # never serve an escalation from cachewith the threat model in-line — a float mtime "can be restored outright with I went looking for exactly that hazard and found it anticipated with a comment and a test. Not approving — my One process note against myself: I first classified your assertions with a keyword grep for negative-sounding words and got "0 fail-closed cases" — badly wrong, as the list above shows. A textual filter can't see what an assertion actually exercises. Reading them was the only thing that worked. |
|
@cla-assistant check |
|
Repaired the current Verified on the pushed head:
Please re-review the exact current head after CI settles. |
23d1b2f to
730ed0b
Compare
|
Rebased #2340 onto current The conflict was the tier-policy evolution on Fresh exact-head validation:
CI is running on |
john-the-dev
left a comment
There was a problem hiding this comment.
APPROVE at the rebased head 730ed0b4. Rui asked me to re-review; I verified the rebase rather than taking the "conflicts resolved" summary on trust, because a conflict-free rebase can still be a semantic break — and this one lands squarely on access-tier logic.
Ran both suites at 730ed0b4, in an isolated worktree with a temp workspace:
tests/gateway-access-path-resolution.test.py PASS — 10 access-path checks
src/remote-gateway-bridge.test.py PASS — all checks green (no regression)
Board at head: MERGEABLE / CLEAN, 0 pending checks, 0 failures, CLA success.
The core change is right, and it is a real fix. Keying _TIER_MAP_CACHE on path (not just the mtime/size/inode identity) is what makes the trust boundary correct: an identity collision across two configs can no longer serve install A's cached grants to install B. The test that pins this — deliberately os.utime-ing the second access.json to the first one's mtime, then asserting the map still re-reads — is exactly the case the old cache got wrong, and it would have passed the old code only by luck.
I also checked the failure directions individually, since they are easy to get backwards: a same-path transient read failure retains last-known-good (correct — a mid-write file must not drop live caps), while a path switch clears it (correct — different config, different trust boundary). Those two want opposite behaviour and the diff gets both.
⚠ One non-blocking observation, on the "disabled" state. Dropping the ~/.claude fallback means that with neither AG2_DEVICE_ENV nor CLAUDE_CONFIG_DIR set, the map resolves to "" and _load_tier_map() returns {}. Because the map only ever caps, _tier_for then falls back to LOCAL_TIER — so a sender explicitly capped in access.json loses that cap. On main that same unconfigured launch would have found ~/.claude/channels/ag2space/access.json, which is the default layout.
I do not think this blocks, for three reasons I checked rather than assumed:
LOCAL_TIERdefaults toowneronly under the personal-agent model (startup.sh:1075, andremote-gateway-bridge.test.py:147pins it) — where there is no local cap to lose anyway. A multi-user gateway setsREMOTE_TASK_TIER=team, so the fallback isteam, notowner.- The supervised launcher always exports
CLAUDE_CONFIG_DIRbefore services start (startup.sh:285), so the disabled state is not reachable on the normal path — only a bare manual launch. - This PR adds the stderr line that announces
access tier map path: disabled (…), so the state is observable instead of silent. That is the right call.
Worth being explicit in a follow-up about whether a multi-user gateway should fail closed (force guest) when the map is disabled, rather than falling back to LOCAL_TIER. Bounded escalation (guest → team), announced, and off the supervised path — a follow-up, not a blocker here.
Merge status: the rebase invalidated the earlier approvals — john-the-dev, liususan091219 and bassilkhilo-ag2 all approved at e86f5919, and there are currently zero at-head approvals. Mine brings it to one. @liususan091219 / @bassilkhilo-ag2, a quick re-confirm at 730ed0b4 would let this merge; the delta from your review is the conflict resolution plus the new path-resolution test.
730ed0b to
a5bc1fe
Compare
|
@keweichen this one is ready to merge except that it's BEHIND main, and I can't update it from this side — the branch is on your fork with Current state as of now: 3 approvals (me, @liususan091219, @bassilkhilo-ag2), If you rebase onto current main and CI stays green, I'll merge it. Alternatively, enabling "Allow edits by maintainers" on the PR lets us keep it current without a round trip each time main moves — this repo requires up-to-date branches, so every merge puts it BEHIND again. |
|
Verified at head The branch was rebased on 2026-08-11, which put every recorded approval at a commit that no longer exists ( Three authored commits, all from 27–28 Jul, all re-committed in one instant — a replay onto newer main with nothing new authored since @liususan091219 and @bassilkhilo-ag2 reviewed. Their approvals are content-current; only the base moved. Tests at head, clean worktree at The two that matter most for an access-control change both hold: a stale home config cannot supply tiers, and a transient read failure retains last-known-good rather than dropping to an empty map. Updating the branch and merging. |
|
@cla-assistant check |
Priority
Level: high
Reason: A desktop-spawned gateway can silently apply a stale tier map from an unrelated install, changing sender trust classification without any diagnostic.
Closes
Closes #2332
Summary
Resolve the ag2space access map from the gateway's active configuration instead of guessing
~/.claude.access.jsonsibling of explicitAG2_DEVICE_ENV.CLAUDE_CONFIG_DIR/channels/ag2space/access.json.HOME.Root cause and user impact
Desktop launchers can provide
AG2_DEVICE_ENVwithoutCLAUDE_CONFIG_DIR. The previous resolver ignored that active device configuration and read~/.claude/channels/ag2space/access.jsoninstead. A stale file there could silently assign a sender the wrong access tier.Before the fix, the focused reproducer showed the configured map was ignored and the stale home-directory map supplied tier
teaminstead of configured tierother.Validation
python3 tests/gateway-access-path-resolution.test.pypython3 tests/gateway-per-sender-tier.test.pypython3 src/remote-gateway-bridge.test.pypython3 tests/remote-gateway-bridge.test.pygit diff --checkscripts/review-checks.shThe new regression coverage checks device-env precedence, the CLAUDE_CONFIG_DIR fallback, no HOME guessing, selected-path logging, correct tier loading, and same-mtime path-switch invalidation.
Edge cases and risk
CLAUDE_CONFIG_DIRis set, existing explicit-config behavior remains.This is a focused resolver/cache change with no migration, permission, prompt, or protocol changes. Rollback is a normal revert.
Review order
packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.pytests/gateway-access-path-resolution.test.pyReview follow-up — current head
c84a78bThe requested cache-disable case is fixed in
f4bb56c: when both launcher config pointers are removed,_load_tier_map()now clears and returns an empty map instead of treating the empty path as a transient stat failure. The regression first loads a mapped sender, removes both pointers, and verifies the sender returns toLOCAL_TIER.Live bare-launch restart evidence
Ran the real
ag2-sparrowentrypoint twice as isolated bare processes against a local relay, withAG2_DEVICE_ENV=/tmp/pr2340-live-evidence/channel/.envandCLAUDE_CONFIG_DIRexplicitly unset. Both starts emitted:The first process queued sender
@teammate:ag2.spaceas:After terminating and restarting the bridge, the second real process again resolved the same sibling map and queued:
This exercises process startup, HTTP task pull, access-path logging, per-sender tier lookup, and task-file serialization—not an in-process import harness.
Post-sync validation on current
main:git diff --check, andscripts/review-checks.sh: passmain