Add bridge bootstrap consume endpoint
This commit is contained in:
parent
0040b940a4
commit
634052af07
144
internal/acp/bootstrap.go
Normal file
144
internal/acp/bootstrap.go
Normal file
@ -0,0 +1,144 @@
|
||||
package acp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"xworkmate-bridge/internal/shared"
|
||||
)
|
||||
|
||||
type bridgeBootstrapConsumeRequest struct {
|
||||
Ticket string `json:"ticket"`
|
||||
Bridge string `json:"bridge"`
|
||||
}
|
||||
|
||||
type accountsBridgeBootstrapConsumeResponse struct {
|
||||
TicketID string `json:"ticketId"`
|
||||
TargetBridge string `json:"targetBridge"`
|
||||
OpenclawURL string `json:"openclawUrl"`
|
||||
AuthMode string `json:"authMode"`
|
||||
ExchangeToken string `json:"exchangeToken"`
|
||||
ExpiresAt string `json:"expiresAt"`
|
||||
Scopes []string `json:"scopes"`
|
||||
}
|
||||
|
||||
type bridgeBootstrapResponse struct {
|
||||
SetupCode string `json:"setupCode"`
|
||||
BridgeOrigin string `json:"bridgeOrigin"`
|
||||
AuthMode string `json:"authMode"`
|
||||
ExpiresAt string `json:"expiresAt"`
|
||||
IssuedBy string `json:"issuedBy"`
|
||||
Scopes []string `json:"scopes"`
|
||||
}
|
||||
|
||||
func (s *Server) HandleBridgeBootstrapHealth(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"ok": true,
|
||||
"bridgeOrigin": bridgePublicBaseURL(),
|
||||
"issuedBy": "xworkmate-bridge",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) HandleBridgeBootstrapConsume(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var req bridgeBootstrapConsumeRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
req.Ticket = strings.TrimSpace(req.Ticket)
|
||||
req.Bridge = strings.TrimSpace(req.Bridge)
|
||||
if req.Ticket == "" {
|
||||
http.Error(w, "ticket is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.Bridge == "" {
|
||||
req.Bridge = bridgePublicBaseURL()
|
||||
}
|
||||
|
||||
payload, status, err := consumeBootstrapFromAccounts(req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), status)
|
||||
return
|
||||
}
|
||||
|
||||
setupCodePayload := map[string]any{
|
||||
"url": payload.OpenclawURL,
|
||||
"token": payload.ExchangeToken,
|
||||
"exchangeToken": payload.ExchangeToken,
|
||||
"authMode": payload.AuthMode,
|
||||
"expiresAt": payload.ExpiresAt,
|
||||
"bridgeOrigin": req.Bridge,
|
||||
"issuedBy": "xworkmate-bridge",
|
||||
}
|
||||
setupCodeBytes, err := json.Marshal(setupCodePayload)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to encode setup payload", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(bridgeBootstrapResponse{
|
||||
SetupCode: string(setupCodeBytes),
|
||||
BridgeOrigin: req.Bridge,
|
||||
AuthMode: payload.AuthMode,
|
||||
ExpiresAt: payload.ExpiresAt,
|
||||
IssuedBy: "xworkmate-bridge",
|
||||
Scopes: append([]string(nil), payload.Scopes...),
|
||||
})
|
||||
}
|
||||
|
||||
func consumeBootstrapFromAccounts(req bridgeBootstrapConsumeRequest) (accountsBridgeBootstrapConsumeResponse, int, error) {
|
||||
baseURL := strings.TrimRight(strings.TrimSpace(shared.EnvOrDefault("ACCOUNTS_BASE_URL", "https://accounts.svc.plus")), "/")
|
||||
if baseURL == "" {
|
||||
return accountsBridgeBootstrapConsumeResponse{}, http.StatusInternalServerError, fmt.Errorf("accounts base url is not configured")
|
||||
}
|
||||
serviceToken := strings.TrimSpace(shared.EnvOrDefault("INTERNAL_SERVICE_TOKEN", ""))
|
||||
if serviceToken == "" {
|
||||
return accountsBridgeBootstrapConsumeResponse{}, http.StatusInternalServerError, fmt.Errorf("internal service token is not configured")
|
||||
}
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return accountsBridgeBootstrapConsumeResponse{}, http.StatusInternalServerError, fmt.Errorf("failed to encode consume request")
|
||||
}
|
||||
httpReq, err := http.NewRequest(http.MethodPost, baseURL+"/api/internal/xworkmate/bridge/bootstrap/consume", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return accountsBridgeBootstrapConsumeResponse{}, http.StatusInternalServerError, fmt.Errorf("failed to create consume request")
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("X-Service-Token", serviceToken)
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return accountsBridgeBootstrapConsumeResponse{}, http.StatusBadGateway, fmt.Errorf("failed to contact accounts service")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return accountsBridgeBootstrapConsumeResponse{}, resp.StatusCode, fmt.Errorf("accounts bootstrap consume failed")
|
||||
}
|
||||
var payload accountsBridgeBootstrapConsumeResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
|
||||
return accountsBridgeBootstrapConsumeResponse{}, http.StatusBadGateway, fmt.Errorf("failed to decode accounts bootstrap response")
|
||||
}
|
||||
return payload, http.StatusOK, nil
|
||||
}
|
||||
|
||||
func bridgePublicBaseURL() string {
|
||||
value := strings.TrimSpace(shared.EnvOrDefault("BRIDGE_PUBLIC_BASE_URL", "https://xworkmate-bridge.svc.plus"))
|
||||
if value == "" {
|
||||
return "https://xworkmate-bridge.svc.plus"
|
||||
}
|
||||
return strings.TrimRight(value, "/")
|
||||
}
|
||||
64
internal/acp/bootstrap_test.go
Normal file
64
internal/acp/bootstrap_test.go
Normal file
@ -0,0 +1,64 @@
|
||||
package acp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHandleBridgeBootstrapConsumeReturnsSetupCode(t *testing.T) {
|
||||
accounts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/internal/xworkmate/bridge/bootstrap/consume" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if got := r.Header.Get("X-Service-Token"); got != "internal-test-token" {
|
||||
http.Error(w, "missing service token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(accountsBridgeBootstrapConsumeResponse{
|
||||
TicketID: "ticket-1",
|
||||
TargetBridge: "https://xworkmate-bridge.svc.plus",
|
||||
OpenclawURL: "wss://openclaw.svc.plus",
|
||||
AuthMode: "shared-token",
|
||||
ExchangeToken: "shared-token-value",
|
||||
ExpiresAt: "2026-04-10T00:00:00Z",
|
||||
Scopes: []string{"connect", "pairing.bootstrap"},
|
||||
})
|
||||
}))
|
||||
defer accounts.Close()
|
||||
|
||||
t.Setenv("ACCOUNTS_BASE_URL", accounts.URL)
|
||||
t.Setenv("INTERNAL_SERVICE_TOKEN", "internal-test-token")
|
||||
t.Setenv("BRIDGE_PUBLIC_BASE_URL", "https://xworkmate-bridge.svc.plus")
|
||||
|
||||
server := NewServer()
|
||||
body := bytes.NewBufferString(`{"ticket":"ticket-1","bridge":"https://xworkmate-bridge.svc.plus"}`)
|
||||
request := httptest.NewRequest(http.MethodPost, "/bridge/bootstrap/consume", body)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
server.HandleBridgeBootstrapConsume(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("expected consume success, got %d: %s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
var payload bridgeBootstrapResponse
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if payload.SetupCode == "" {
|
||||
t.Fatalf("expected non-empty setup code")
|
||||
}
|
||||
var decoded map[string]any
|
||||
if err := json.Unmarshal([]byte(payload.SetupCode), &decoded); err != nil {
|
||||
t.Fatalf("decode setup code payload: %v", err)
|
||||
}
|
||||
if decoded["url"] != "wss://openclaw.svc.plus" {
|
||||
t.Fatalf("expected openclaw url in setup payload, got %#v", decoded)
|
||||
}
|
||||
if decoded["token"] != "shared-token-value" {
|
||||
t.Fatalf("expected exchange token in setup payload, got %#v", decoded)
|
||||
}
|
||||
}
|
||||
@ -75,6 +75,13 @@ func Serve(args []string) error {
|
||||
Addr: strings.TrimSpace(*listen),
|
||||
Handler: 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 "/bridge/bootstrap/health":
|
||||
server.HandleBridgeBootstrapHealth(w, r)
|
||||
case "/bridge/bootstrap/consume":
|
||||
server.HandleBridgeBootstrapConsume(w, r)
|
||||
case "/acp/rpc":
|
||||
server.HandleRPC(w, r)
|
||||
case "/acp":
|
||||
@ -848,11 +855,11 @@ func (s *Server) runSingleAgent(
|
||||
|
||||
return taskResult{
|
||||
response: enrichSingleAgentResultArtifacts(map[string]any{
|
||||
"success": true,
|
||||
"output": output,
|
||||
"turnId": turnID,
|
||||
"mode": "single-agent",
|
||||
"provider": provider,
|
||||
"success": true,
|
||||
"output": output,
|
||||
"turnId": turnID,
|
||||
"mode": "single-agent",
|
||||
"provider": provider,
|
||||
"effectiveWorkingDirectory": effectiveWorkingDirectory,
|
||||
}, params),
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user