fix(core): guard external A2A loopback routing #1632

Merged
hongming merged 1 commits from test/e2e-chat-a2a-dns-regression into main 2026-05-21 06:59:27 +00:00
2 changed files with 44 additions and 4 deletions
@@ -686,11 +686,22 @@ func (h *WorkspaceHandler) resolveAgentURL(ctx context.Context, workspaceID stri
_ = db.CacheURL(ctx, workspaceID, agentURL)
}
// When the platform runs inside Docker, 127.0.0.1:{host_port} is
// unreachable (it's the platform container's own localhost, not the
// Docker host). Rewrite to the container's Docker-bridge hostname.
// When the platform runs inside Docker, a managed workspace's
// 127.0.0.1:{host_port} URL points at the Docker host and must be
// rewritten to the workspace container's Docker-bridge hostname.
// External runtimes are not managed containers; their local test/runtime
// URL is the target and must not be synthesized into ws-<id>:8000.
if strings.HasPrefix(agentURL, "http://127.0.0.1:") && h.provisioner != nil && platformInDocker {
agentURL = provisioner.InternalURL(workspaceID)
var wsRuntime string
if err := db.DB.QueryRowContext(ctx,
`SELECT COALESCE(runtime, 'langgraph') FROM workspaces WHERE id = $1`,
workspaceID,
).Scan(&wsRuntime); err != nil {
log.Printf("ProxyA2A: runtime lookup before Docker URL rewrite failed for %s: %v", workspaceID, err)
}
if !isExternalLikeRuntime(wsRuntime) {
agentURL = provisioner.InternalURL(workspaceID)
}
}
// SSRF defence: reject private/metadata URLs before making outbound call.
if err := isSafeURL(agentURL); err != nil {
@@ -1511,6 +1511,35 @@ func TestResolveAgentURL_DockerRewrite(t *testing.T) {
}
}
func TestResolveAgentURL_ExternalRuntimeLoopbackNotRewrittenInDocker(t *testing.T) {
mock := setupTestDB(t)
mr := setupTestRedis(t)
allowLoopbackForTest(t)
handler := NewWorkspaceHandler(newTestBroadcaster(), nil, "http://localhost:8080", t.TempDir())
waitForHandlerAsyncBeforeDBCleanup(t, handler)
handler.provisioner = &stubLocalProv{}
restore := setPlatformInDockerForTest(true)
defer restore()
agentURL := "http://127.0.0.1:55555"
mr.Set("ws:ws-external:url", agentURL)
mock.ExpectQuery("SELECT COALESCE\\(runtime").
WithArgs("ws-external").
WillReturnRows(sqlmock.NewRows([]string{"runtime"}).AddRow("external"))
url, perr := handler.resolveAgentURL(context.Background(), "ws-external")
if perr != nil {
t.Fatalf("unexpected error: %+v", perr)
}
if url != agentURL {
t.Errorf("external runtime loopback URL must not be rewritten; got %q want %q", url, agentURL)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet sqlmock expectations: %v", err)
}
}
// --- dispatchA2A direct unit tests ---
func TestDispatchA2A_BuildRequestError(t *testing.T) {