package acp import ( "bytes" "context" "encoding/json" "fmt" "io" "log" "net/http" "strings" "sync" "sync/atomic" "time" "xworkmate-bridge/internal/shared" ) var httpSSEKeepaliveInterval = 20 * time.Second const openClawGatewayMaxNotificationBytes = 64 * 1024 func (s *Server) Handler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/": w.Header().Set("Content-Type", "text/plain; charset=utf-8") _, _ = w.Write([]byte("xworkmate-bridge is running")) case "/api/ping": if !s.authorized(r) { shared.WriteJSONError(w, nil, http.StatusUnauthorized, -32001, "missing bearer authorization") return } info := CurrentRuntimeVersionInfo() resp := map[string]any{ "status": "ok", "commit": info.Commit, "version": info.Version, "buildDate": info.BuildDate, } body, _ := json.Marshal(resp) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _, _ = w.Write(body) case "/acp/rpc": s.HandleRPC(w, r) case "/acp": s.HandleWebSocket(w, r) case "/gateway/openclaw": s.HandleOpenClawGatewayRPC(w, r) case openClawArtifactDownloadPath: s.HandleOpenClawArtifactDownload(w, r) default: if strings.HasPrefix(r.URL.Path, "/acp-server/") { s.HandleDisabledProviderDirectPath(w, r) return } http.NotFound(w, r) } }) } func (s *Server) HandleWebSocket(w http.ResponseWriter, r *http.Request) { origin := strings.TrimSpace(r.Header.Get("Origin")) if !shared.OriginAllowed(origin, s.allowedOrigins) { shared.WriteJSONError(w, nil, http.StatusForbidden, -32003, fmt.Sprintf("origin not allowed: %s", origin)) return } if !s.authorized(r) { shared.WriteJSONError(w, nil, http.StatusUnauthorized, -32001, "missing bearer authorization") return } upgrader := shared.StandardWSUpgrader upgrader.CheckOrigin = func(req *http.Request) bool { return shared.OriginAllowed(req.Header.Get("Origin"), s.allowedOrigins) && s.authorized(req) } conn, err := upgrader.Upgrade(w, r, nil) if err != nil { return } defer func() { _ = conn.Close() }() var writeMu sync.Mutex notify := func(message map[string]any) { writeMu.Lock() defer writeMu.Unlock() _ = conn.WriteJSON(message) } for { _, payload, err := conn.ReadMessage() if err != nil { return } request, err := shared.DecodeRPCRequest(payload) if err != nil { notify(shared.ErrorEnvelope(nil, -32700, err.Error())) continue } request.Params = injectInboundAuthorizationHeader(request.Params, r.Header.Get("Authorization")) response, rpcErr := s.handleRequest(request, notify) if request.ID == nil { continue } if rpcErr != nil { notify(shared.ErrorEnvelope(request.ID, rpcErr.Code, rpcErr.Message, rpcErr.Data)) continue } notify(shared.ResultEnvelope(request.ID, response)) } } func (s *Server) HandleRPC(w http.ResponseWriter, r *http.Request) { s.handleRPCWithTransform(w, r, nil) } func (s *Server) HandleOpenClawGatewayRPC(w http.ResponseWriter, r *http.Request) { s.handleRPCWithTransform(w, r, forceOpenClawGatewayRequest) } func (s *Server) HandleDisabledProviderDirectPath(w http.ResponseWriter, r *http.Request) { shared.ApplyCORS(w, r, s.allowedOrigins) if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } if !s.authorized(r) { shared.WriteJSONError(w, nil, http.StatusUnauthorized, -32001, "missing bearer authorization") return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusGone) _ = json.NewEncoder(w).Encode(map[string]any{ "jsonrpc": "2.0", "error": map[string]any{ "code": -32004, "message": "PROVIDER_DIRECT_PATH_DISABLED: use /acp/rpc provider catalog and routing", }, "type": "res", "ok": false, }) } func (s *Server) handleRPCWithTransform( w http.ResponseWriter, r *http.Request, transform func(shared.RPCRequest) (shared.RPCRequest, *shared.RPCError), ) { shared.ApplyCORS(w, r, s.allowedOrigins) if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } if r.Method != http.MethodPost { shared.WriteJSONError(w, nil, http.StatusMethodNotAllowed, -32600, "method not allowed") return } origin := strings.TrimSpace(r.Header.Get("Origin")) if !shared.OriginAllowed(origin, s.allowedOrigins) { shared.WriteJSONError(w, nil, http.StatusForbidden, -32003, fmt.Sprintf("origin not allowed: %s", origin)) return } payload, err := io.ReadAll(r.Body) if err != nil { shared.WriteJSONError(w, nil, http.StatusBadRequest, -32600, "invalid body") return } r.Body = io.NopCloser(bytes.NewBuffer(payload)) if !s.authorized(r) { shared.WriteJSONError(w, nil, http.StatusUnauthorized, -32001, "missing bearer authorization") return } request, err := shared.DecodeRPCRequest(payload) if err != nil { shared.WriteJSONError(w, nil, http.StatusBadRequest, -32700, err.Error()) return } request.Params = injectInboundAuthorizationHeader(request.Params, r.Header.Get("Authorization")) if transform != nil { transformed, rpcErr := transform(request) if rpcErr != nil { w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(shared.ErrorEnvelope(request.ID, rpcErr.Code, rpcErr.Message, rpcErr.Data)) return } request = transformed } if s.taskRouter.forward(r.Context(), w, r, request) { return } accept := strings.ToLower(r.Header.Get("Accept")) stream := strings.Contains(accept, "text/event-stream") openClawGatewayTask := requestUsesOpenClawGatewaySubmit( shared.AsMap(request.Params), ) if stream { w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") } streamWriter := newSafeSSEStream(r.Context(), w, safeSSEStreamMeta{ Path: r.URL.Path, Method: request.Method, SessionID: shared.StringArg(request.Params, "sessionId", ""), ThreadID: shared.StringArg(request.Params, "threadId", ""), RequestID: fmt.Sprint(request.ID), }) stopKeepalive := func() {} writeNotification := func(message map[string]any) { if !stream { return } if reason := gatewaySSEBridgeNotificationDropReason( request.Method, openClawGatewayTask, message, ); reason != "" { log.Printf( "level=warn component=acp_sse event=notification_dropped path=%q rpcMethod=%q requestId=%q sessionId=%q threadId=%q reason=%q notificationMethod=%q", r.URL.Path, request.Method, fmt.Sprint(request.ID), shared.StringArg(request.Params, "sessionId", ""), shared.StringArg(request.Params, "threadId", ""), reason, shared.StringArg(message, "method", ""), ) return } streamWriter.write(message) } if stream { if openClawGatewayTask { streamWriter.write(map[string]any{ "jsonrpc": "2.0", "method": "xworkmate.bridge.accepted", "params": map[string]any{ "sessionId": shared.StringArg(request.Params, "sessionId", ""), "threadId": shared.StringArg(request.Params, "threadId", ""), "method": request.Method, "path": r.URL.Path, "acceptedAt": time.Now().UTC().Format(time.RFC3339Nano), }, }) } stopKeepalive = streamWriter.startKeepalive(httpSSEKeepaliveInterval) } defer stopKeepalive() defer streamWriter.close() response, rpcErr := s.handleRequest(request, writeNotification) stopKeepalive() if request.ID == nil { if stream { streamWriter.done() } return } if rpcErr != nil { envelope := shared.ErrorEnvelope(request.ID, rpcErr.Code, rpcErr.Message, rpcErr.Data) if stream { streamWriter.write(envelope) streamWriter.done() return } w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(envelope) return } if openClawGatewayTask { stripOpenClawArtifactInlineContent(response) } if stream { streamWriter.write(shared.ResultEnvelope(request.ID, response)) streamWriter.done() return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(shared.ResultEnvelope(request.ID, response)) } func gatewaySSEBridgeNotificationDropReason( rpcMethod string, openClawGatewayTask bool, message map[string]any, ) string { method := strings.TrimSpace(shared.StringArg(message, "method", "")) if strings.HasPrefix(method, "xworkmate.gateway.") && (openClawGatewayTask || strings.TrimSpace(rpcMethod) == "xworkmate.gateway.request") { return "raw_gateway_event" } if !openClawGatewayNotificationWithinLimit(message) { return "oversized" } return "" } func openClawGatewayNotificationWithinLimit(message map[string]any) bool { if message == nil { return true } body, err := json.Marshal(message) if err != nil { return false } return len(body) <= openClawGatewayMaxNotificationBytes } type safeSSEStream struct { ctx context.Context w http.ResponseWriter flusher http.Flusher meta safeSSEStreamMeta closed atomic.Bool mu sync.Mutex } type safeSSEStreamMeta struct { Path string Method string RequestID string SessionID string ThreadID string } func newSafeSSEStream(ctx context.Context, w http.ResponseWriter, meta safeSSEStreamMeta) *safeSSEStream { flusher, _ := w.(http.Flusher) return &safeSSEStream{ctx: ctx, w: w, flusher: flusher, meta: meta} } func (s *safeSSEStream) write(payload map[string]any) bool { return s.writeRaw(sseEventType(payload), func() error { return shared.WriteSSE(s.w, payload) }) } func (s *safeSSEStream) done() bool { return s.writeRaw("done", func() error { _, err := s.w.Write([]byte("data: [DONE]\n\n")) return err }) } func (s *safeSSEStream) startKeepalive(interval time.Duration) func() { if s == nil || interval <= 0 { return func() {} } done := make(chan struct{}) var stopOnce sync.Once go func() { ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ticker.C: if !s.write(map[string]any{ "jsonrpc": "2.0", "method": "xworkmate.bridge.keepalive", "params": map[string]any{ "intervalMs": interval.Milliseconds(), }, }) { return } case <-done: return } } }() return func() { stopOnce.Do(func() { close(done) }) } } func (s *safeSSEStream) close() { s.closed.Store(true) } func (s *safeSSEStream) writeRaw(eventType string, write func() error) (ok bool) { if s == nil || s.closed.Load() { return false } select { case <-s.ctx.Done(): s.closed.Store(true) s.logWriteFailure(eventType, "context_done", s.ctx.Err()) return false default: } s.mu.Lock() defer s.mu.Unlock() if s.closed.Load() { return false } defer func() { if recovered := recover(); recovered != nil { s.closed.Store(true) s.logWriteFailure(eventType, "panic", fmt.Errorf("%v", recovered)) ok = false } }() if err := write(); err != nil { s.closed.Store(true) s.logWriteFailure(eventType, "write_failed", err) return false } if s.flusher != nil { s.flusher.Flush() } return true } func (s *safeSSEStream) logWriteFailure(eventType string, reason string, err error) { if s == nil { return } errText := "" if err != nil { errText = err.Error() } log.Printf( "level=warn component=acp_sse event=stream_write path=%q rpcMethod=%q requestId=%q sessionId=%q threadId=%q sseEvent=%q reason=%q error=%q", s.meta.Path, s.meta.Method, s.meta.RequestID, s.meta.SessionID, s.meta.ThreadID, eventType, reason, errText, ) } func sseEventType(payload map[string]any) string { if payload == nil { return "unknown" } if method, _ := payload["method"].(string); strings.TrimSpace(method) != "" { return strings.TrimSpace(method) } if payload["result"] != nil { return "result" } if payload["error"] != nil { return "error" } return "unknown" } func forceOpenClawGatewayRequest(request shared.RPCRequest) (shared.RPCRequest, *shared.RPCError) { method := strings.TrimSpace(request.Method) switch method { case "session.start", "session.message": default: return request, &shared.RPCError{Code: -32601, Message: "OPENCLAW_GATEWAY_METHOD_NOT_ALLOWED: " + method} } params := shared.AsMap(request.Params) if params == nil { params = map[string]any{} } if provider := strings.TrimSpace(shared.StringArg(params, "provider", "")); provider != "" { return request, &shared.RPCError{Code: -32602, Message: "OPENCLAW_GATEWAY_CONFLICT: provider must not be set on /gateway/openclaw"} } for _, key := range []string{"executionTarget", "requestedExecutionTarget"} { if target := strings.TrimSpace(shared.StringArg(params, key, "")); target != "" && !strings.EqualFold(target, "gateway") { return request, &shared.RPCError{Code: -32602, Message: "OPENCLAW_GATEWAY_CONFLICT: " + key + " must be gateway"} } } for _, key := range []string{"preferredGatewayProviderId", "gatewayProviderId", "gatewayProvider"} { if provider := strings.TrimSpace(shared.StringArg(params, key, "")); provider != "" && !strings.EqualFold(provider, "openclaw") { return request, &shared.RPCError{Code: -32602, Message: "OPENCLAW_GATEWAY_CONFLICT: gateway provider must be openclaw"} } } routing := shared.AsMap(params["routing"]) if routing == nil { routing = map[string]any{} } if provider := strings.TrimSpace(shared.StringArg(routing, "explicitProviderId", "")); provider != "" { return request, &shared.RPCError{Code: -32602, Message: "OPENCLAW_GATEWAY_CONFLICT: explicitProviderId must not be set on /gateway/openclaw"} } if target := strings.TrimSpace(shared.StringArg(routing, "explicitExecutionTarget", "")); target != "" && !strings.EqualFold(target, "gateway") { return request, &shared.RPCError{Code: -32602, Message: "OPENCLAW_GATEWAY_CONFLICT: explicitExecutionTarget must be gateway"} } for _, key := range []string{"preferredGatewayProviderId", "gatewayProviderId", "gatewayProvider"} { if provider := strings.TrimSpace(shared.StringArg(routing, key, "")); provider != "" && !strings.EqualFold(provider, "openclaw") { return request, &shared.RPCError{Code: -32602, Message: "OPENCLAW_GATEWAY_CONFLICT: gateway provider must be openclaw"} } } routing["routingMode"] = "explicit" routing["explicitExecutionTarget"] = "gateway" routing["preferredGatewayProviderId"] = "openclaw" delete(routing, "explicitProviderId") params["routing"] = routing params["requestedExecutionTarget"] = "gateway" params["executionTarget"] = "gateway" request.Params = params return request, nil } func requestUsesOpenClawGatewaySubmit(params map[string]any) bool { if len(params) == 0 { return false } if requestHasExplicitAgentRouting(params) { return false } for _, key := range []string{"executionTarget", "requestedExecutionTarget"} { if isGatewayExecutionTarget(shared.StringArg(params, key, "")) { return true } } for _, key := range []string{"gatewayProvider", "gatewayProviderId"} { if isOpenClawProvider(shared.StringArg(params, key, "")) { return true } } routing := shared.AsMap(params["routing"]) if isGatewayExecutionTarget(shared.StringArg(routing, "explicitExecutionTarget", "")) { return true } for _, key := range []string{"preferredGatewayProviderId", "gatewayProviderId", "gatewayProvider"} { if isOpenClawProvider(shared.StringArg(routing, key, "")) { return true } } return false } func requestHasExplicitAgentRouting(params map[string]any) bool { for _, key := range []string{"executionTarget", "requestedExecutionTarget"} { if isAgentExecutionTarget(shared.StringArg(params, key, "")) { return true } } if provider := strings.TrimSpace(shared.StringArg(params, "provider", "")); provider != "" && !isOpenClawProvider(provider) { return true } routing := shared.AsMap(params["routing"]) if isAgentExecutionTarget(shared.StringArg(routing, "explicitExecutionTarget", "")) { return true } if provider := strings.TrimSpace(shared.StringArg(routing, "explicitProviderId", "")); provider != "" && !isOpenClawProvider(provider) { return true } return false } func isAgentExecutionTarget(value string) bool { normalized := strings.ToLower(strings.TrimSpace(value)) return normalized == "agent" || normalized == "single-agent" || normalized == "singleagent" } func isGatewayExecutionTarget(value string) bool { return strings.EqualFold(strings.TrimSpace(value), "gateway") } func isOpenClawProvider(value string) bool { return strings.EqualFold(strings.TrimSpace(value), "openclaw") } func (s *Server) authorized(r *http.Request) bool { if s == nil { return false } if s.authService == nil { return true } type validator interface { ValidateAuthorizationHeader(string) bool } if v, ok := s.authService.(validator); ok { return v.ValidateAuthorizationHeader(r.Header.Get("Authorization")) } return true } func injectInboundAuthorizationHeader(params map[string]any, authorization string) map[string]any { if params == nil { params = map[string]any{} } authorization = strings.TrimSpace(authorization) if authorization != "" { params["bridgeAuthorizationHeader"] = authorization } return params }