MCPcopy Create free account
hub / github.com/Mnexa-AI/e2a / serve

Method serve

internal/ws/handler.go:51–128  ·  view source on GitHub ↗
(w http.ResponseWriter, r *http.Request, rawEmail string)

Source from the content-addressed store, hash-verified

49}
50
51func (h *Handler) serve(w http.ResponseWriter, r *http.Request, rawEmail string) {
52 // Authenticate via the `Authorization: Bearer <api_key>` handshake header —
53 // the standard shape for non-browser WebSocket clients (matches OpenAI
54 // Realtime server-side, Slack Socket Mode, Kubernetes). The credential never
55 // touches the URL, so it can't leak to access logs / browser history /
56 // Referer the way a `?token=` query param does. All e2a WS clients (the TS +
57 // Python SDKs and the CLI) are non-browser and set the header on the
58 // handshake. (A short-lived connect ticket is the path to add later if an
59 // in-browser client ever needs to connect — browsers can't set headers.)
60 token := bearerToken(r)
61 if token == "" {
62 w.Header().Set("WWW-Authenticate", `Bearer realm="e2a"`)
63 http.Error(w, "missing credential — send Authorization: Bearer <api_key>", http.StatusUnauthorized)
64 return
65 }
66
67 principal, err := h.store.GetPrincipalByAPIKey(r.Context(), token)
68 if err != nil || principal == nil || principal.User == nil {
69 http.Error(w, "invalid token", http.StatusUnauthorized)
70 return
71 }
72
73 // Resolve agent and verify ownership. Canonicalize the email so that
74 // `ws://.../UPPER@x.dev/ws?token=…` resolves identically to the
75 // lower-case form — matches the REST API's `normalizeEmail` policy.
76 email := identity.NormalizeEmail(rawEmail)
77 agent, err := h.store.GetAgentByEmail(r.Context(), email)
78 if err != nil {
79 http.Error(w, "agent not found", http.StatusNotFound)
80 return
81 }
82 // Tenant ownership: the agent must belong to the credential's user.
83 if agent.UserID != principal.User.ID {
84 http.Error(w, "not authorized for this agent", http.StatusForbidden)
85 return
86 }
87 // Agent-scope confinement (HIGH-1): an agent-scoped credential is pinned to
88 // its one bound agent — it may not open a different agent's stream even
89 // within the same account. Mirrors the REST resolveOwnedAgent pin.
90 if principal.Scope == identity.ScopeAgent && principal.AgentID != agent.ID {
91 http.Error(w, "not authorized for this agent", http.StatusForbidden)
92 return
93 }
94
95 // Upgrade to WebSocket. Origin checks are intentionally disabled because
96 // authentication is the `Authorization: Bearer` header, not cookies — and a
97 // browser cannot set that header on a cross-site WebSocket, so there is no
98 // cross-site-WebSocket-hijacking (CSWSH) vector to defend against here.
99 // CLI/SDK clients have no Origin to check. (Header auth is strictly safer
100 // than the old `?token=` query param it replaced, which leaked the key to
101 // access logs / history / Referer.)
102 conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
103 InsecureSkipVerify: true,
104 })
105 if err != nil {
106 log.Printf("[ws] upgrade failed for %s: %v", email, err)
107 return
108 }

Callers 2

HandleMethod · 0.95
ServeWithEmailMethod · 0.95

Calls 10

drainUnreadMethod · 0.95
readLoopMethod · 0.95
NormalizeEmailFunction · 0.92
bearerTokenFunction · 0.85
RegisterMethod · 0.80
UnregisterMethod · 0.80
GetPrincipalByAPIKeyMethod · 0.65
GetAgentByEmailMethod · 0.65
ErrorMethod · 0.45
CloseMethod · 0.45

Tested by

no test coverage detected