Repository navigation
Fix: Fold a redirected bridged CONNECT into its inner request in agentop - #1594
Conversation
isBridgedInner matched the inner request's Host against the tunnel's CONNECT target, so a request the inference-router redirected to another server never folded: the tunnel-open stood as its own blank-method row naming the original server, with no response row (the inner request carries it), roughly doubling the timeline and reading as if traffic still went to the host the client named. A redirected inner row carries the client's host in RequestedHost — the CONNECT's target — so accept that as the second match arm. RequestedHost is present only on a redirected request, so an unredirected request still cannot fold onto someone else's tunnel. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 24 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Fixes review: isBridgedInner only folded SessionRequest, so a bridged tunnel whose first decrypted row was a denial (a pipeline plugin Reject, recorded before any request row) kept its orphan tunnel-open row. Files: cmd/agentop/tui/events_pane.go, cmd/agentop/tui/events_pane_test.go Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
clawgenti
left a comment
There was a problem hiding this comment.
Summary
The fix is correct and narrowly scoped. isBridgedInner now matches on either Host or RequestedHost against the CONNECT target, and it accepts a SessionDenied inner row. That matches what core/session/timeline/hosts.go already does in settle(), so agentop and the server-side timeline now agree. The guard ordering is safe: direction, phase and tunnel-open are checked before either host arm, and pipeline.Context.RequestedHost() returns "" unless a redirect actually moved the request. So the new arm can't fold an unredirected request onto a tunnel for a different host, and the existing pooling and host-mismatch tests still cover the old guards. I checked the denial-first case against tunnelLog.recordWith / recordOutboundRejectIn in the forward proxy. The deferred open really is recorded with whichever row comes first, so folding a denial is correct.
I have no blocking issues, only two small test suggestions inline.
Author: huang195 (MEMBER, maintainer)
Areas reviewed: Go (agentop TUI), tests, and a parity check against core/session/timeline and forwardproxy
Agent/IDE config (.claude/.vscode): none
Commits: 2 commits, all signed off: yes
CI status: passing (all required checks green; CodeRabbit/Spellcheck skipped)
| // RequestedHost is present only when a redirect moved the request, so this | ||
| // arm cannot fold an unredirected request onto someone else's tunnel. | ||
| return inner.RequestedHost != "" && | ||
| hostOnly(inner.RequestedHost) == hostOnly(tunnel.Host) |
There was a problem hiding this comment.
suggestion: The new arm has no negative test. Consider adding a case to TestBuildEventRows_PassthroughTunnelStandsAlone: a CONNECT to a.example:443 followed by a redirected request with Host: b.example and RequestedHost: c.example:443. It should not fold. Today the RequestedHost != "" / host-equality pairing is only tested for the positive case, so a later change that relaxes this arm (for example, dropping the equality) would still pass every test.
A minor point on parity: the server-side HostKey lowercases the host and strips only :443, while hostOnly is case-sensitive and strips any port. This was already true before this PR, so it isn't blocking. It does mean a mixed-case CONNECT target could fold on the server and not in agentop. An strings.EqualFold here would close that gap if you want it.
| // Each subtest covers one match arm: unredirected denial (Host) and redirected | ||
| // denial (RequestedHost). | ||
| func TestBuildEventRows_CollapsesBridgedConnectDenied(t *testing.T) { | ||
| for name, inner := range map[string]pipeline.SessionEvent{ |
There was a problem hiding this comment.
nit: The loop is over a map without t.Run, so the first t.Fatalf stops the remaining case and the failure shows up as one undifferentiated test. Wrapping the body in t.Run(name, func(t *testing.T) { ... }) lets each match arm pass or fail on its own (and then the %s: prefixes aren't needed). It would also match the table style of PassthroughTunnelStandsAlone below.
What
agentop's CONNECT fold skipped requests that a plugin redirected to another inference server, so a redirected session rendered one orphan tunnel-open row per request — blank METHOD, blank STATUS, naming the original server, with no response row (the inner request carries it). The timeline roughly doubled and read as if traffic still went to the host the client named.Why
isBridgedInnermatched the inner request'sHostagainst the tunnel's CONNECT target. A redirected inner row'sHostis the dial target (where the bytes went), so the match failed and the open stood alone.A redirected row carries the client's host in
RequestedHost— which is exactly the CONNECT's target — so that is now the second match arm.RequestedHostis present only on a redirected request, so an unredirected request still cannot fold onto someone else's tunnel; the pooling and host-mismatch guards keep their tests.The fold also covers a tunnel whose first decrypted row is a denial rather than a request — the proxy defers a bridged open and records it with whichever row comes first, and a pipeline plugin's Reject (recorded before any request row) can be that row. Both match arms apply; the server-side timeline (
core/session/timeline) already folded this shape, so the events pane now matches it.Repro (before)
A session pinned to another server via the inference-router produced, per request:
After, rows 167+168 are one row carrying the tunnel in its detail pane.
Testing
TestBuildEventRows_CollapsesBridgedConnectRedirected(new): redirected bridged pair folds to one row keyed on the inner request, tunnel attached.TestBuildEventRows_CollapsesBridgedConnect/PassthroughTunnelStandsAloneunchanged and passing (the pooling guard caught a bad first draft of this change — the guards are load-bearing).go test ./tui/green,gofmt/go vetclean.Assisted-By: Claude (Anthropic AI) noreply@anthropic.com