Compare commits

...

1 Commits

Author SHA1 Message Date
Haitao Pan
d86463ef8a Add shared XWorkmate bridge defaults 2026-04-09 10:30:26 +08:00
2 changed files with 183 additions and 22 deletions

View File

@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"os"
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -26,14 +27,16 @@ type xworkmateAccessContext struct {
} }
type xworkmateProfilePayload struct { type xworkmateProfilePayload struct {
OpenclawURL string `json:"openclawUrl"` OpenclawURL string `json:"openclawUrl"`
OpenclawOrigin string `json:"openclawOrigin"` OpenclawOrigin string `json:"openclawOrigin"`
VaultURL string `json:"vaultUrl"` VaultURL string `json:"vaultUrl"`
VaultNamespace string `json:"vaultNamespace"` VaultNamespace string `json:"vaultNamespace"`
VaultSecretPath string `json:"vaultSecretPath"` VaultSecretPath string `json:"vaultSecretPath"`
VaultSecretKey string `json:"vaultSecretKey"` VaultSecretKey string `json:"vaultSecretKey"`
SecretLocators []xworkmateSecretLocatorPayload `json:"secretLocators"` SecretLocators []xworkmateSecretLocatorPayload `json:"secretLocators"`
ApisixURL string `json:"apisixUrl"` ApisixURL string `json:"apisixUrl"`
BridgeServerURL string `json:"bridgeServerUrl"`
AcpBridgeServerProfiles []xworkmateAcpBridgeServerProfilePayload `json:"acpBridgeServerProfiles"`
} }
type xworkmateSecretLocatorPayload struct { type xworkmateSecretLocatorPayload struct {
@ -45,6 +48,15 @@ type xworkmateSecretLocatorPayload struct {
Required bool `json:"required"` Required bool `json:"required"`
} }
type xworkmateAcpBridgeServerProfilePayload struct {
ProviderKey string `json:"providerKey"`
Label string `json:"label"`
Badge string `json:"badge"`
Endpoint string `json:"endpoint"`
AuthRef string `json:"authRef"`
Enabled bool `json:"enabled"`
}
var xworkmateForbiddenTokenFields = map[string]struct{}{ var xworkmateForbiddenTokenFields = map[string]struct{}{
"openclawtoken": {}, "openclawtoken": {},
"gatewaytoken": {}, "gatewaytoken": {},
@ -365,16 +377,80 @@ func (h *handler) buildSessionUser(ctx context.Context, host string, user *store
return payload, nil return payload, nil
} }
func buildXWorkmateProfileResponse(access *xworkmateAccessContext, profile *store.XWorkmateProfile, tokenConfigured gin.H) gin.H { func envXWorkmateValue(key string) string {
return strings.TrimSpace(os.Getenv(key))
}
func buildSharedXWorkmateAcpBridgeServerProfiles() []gin.H {
profiles := make([]gin.H, 0, 2)
appendProfile := func(providerKey, label, badge, endpointEnv, authRefEnv string) {
endpoint := envXWorkmateValue(endpointEnv)
if endpoint == "" {
return
}
profiles = append(profiles, gin.H{
"providerKey": providerKey,
"label": label,
"badge": badge,
"endpoint": endpoint,
"authRef": envXWorkmateValue(authRefEnv),
"enabled": true,
})
}
appendProfile("codex", "Codex", "Codex", "XWORKMATE_ACP_CODEX_URL", "XWORKMATE_ACP_CODEX_AUTH_REF")
appendProfile("opencode", "OpenCode", "OpenCode", "XWORKMATE_ACP_OPENCODE_URL", "XWORKMATE_ACP_OPENCODE_AUTH_REF")
return profiles
}
func buildResolvedXWorkmateProfile(access *xworkmateAccessContext, profile *store.XWorkmateProfile) (*store.XWorkmateProfile, string, []gin.H) {
var resolved store.XWorkmateProfile
if profile != nil {
resolved = *profile
resolved.SecretLocators = append([]store.XWorkmateSecretLocator{}, profile.SecretLocators...)
} else {
resolved = store.XWorkmateProfile{}
}
bridgeServerURL := ""
acpBridgeServerProfiles := []gin.H{}
if access != nil && access.ProfileScope == store.XWorkmateProfileScopeTenantShared {
if resolved.OpenclawURL == "" {
resolved.OpenclawURL = envXWorkmateValue("XWORKMATE_OPENCLAW_URL")
}
if resolved.OpenclawOrigin == "" {
resolved.OpenclawOrigin = envXWorkmateValue("XWORKMATE_OPENCLAW_ORIGIN")
}
if resolved.VaultURL == "" {
resolved.VaultURL = envXWorkmateValue("XWORKMATE_VAULT_ADDR")
}
if resolved.VaultNamespace == "" {
resolved.VaultNamespace = envXWorkmateValue("XWORKMATE_VAULT_NAMESPACE")
}
if resolved.ApisixURL == "" {
resolved.ApisixURL = envXWorkmateValue("XWORKMATE_APISIX_URL")
}
bridgeServerURL = envXWorkmateValue("XWORKMATE_BRIDGE_SERVER_URL")
acpBridgeServerProfiles = buildSharedXWorkmateAcpBridgeServerProfiles()
}
return &resolved, bridgeServerURL, acpBridgeServerProfiles
}
func buildXWorkmateProfileResponse(access *xworkmateAccessContext, profile *store.XWorkmateProfile, bridgeServerURL string, acpBridgeServerProfiles []gin.H, tokenConfigured gin.H) gin.H {
resolvedProfile := gin.H{ resolvedProfile := gin.H{
"openclawUrl": "", "openclawUrl": "",
"openclawOrigin": "", "openclawOrigin": "",
"vaultUrl": "", "vaultUrl": "",
"vaultNamespace": "", "vaultNamespace": "",
"vaultSecretPath": "", "vaultSecretPath": "",
"vaultSecretKey": "", "vaultSecretKey": "",
"secretLocators": []gin.H{}, "secretLocators": []gin.H{},
"apisixUrl": "", "apisixUrl": "",
"bridgeServerUrl": bridgeServerURL,
"acpBridgeServerProfiles": acpBridgeServerProfiles,
} }
if profile != nil { if profile != nil {
resolvedProfile["openclawUrl"] = profile.OpenclawURL resolvedProfile["openclawUrl"] = profile.OpenclawURL
@ -615,8 +691,9 @@ func (h *handler) getXWorkmateProfile(c *gin.Context) {
} }
tokenConfigured = buildXWorkmateTokenConfiguredWithVaultStatus(profile, statusByTarget) tokenConfigured = buildXWorkmateTokenConfiguredWithVaultStatus(profile, statusByTarget)
} }
resolvedProfile, bridgeServerURL, acpBridgeServerProfiles := buildResolvedXWorkmateProfile(access, profile)
c.JSON(http.StatusOK, buildXWorkmateProfileResponse(access, profile, tokenConfigured)) c.JSON(http.StatusOK, buildXWorkmateProfileResponse(access, resolvedProfile, bridgeServerURL, acpBridgeServerProfiles, tokenConfigured))
} }
func (h *handler) updateXWorkmateProfile(c *gin.Context) { func (h *handler) updateXWorkmateProfile(c *gin.Context) {
@ -705,8 +782,9 @@ func (h *handler) updateXWorkmateProfile(c *gin.Context) {
} }
tokenConfigured = buildXWorkmateTokenConfiguredWithVaultStatus(profile, statusByTarget) tokenConfigured = buildXWorkmateTokenConfiguredWithVaultStatus(profile, statusByTarget)
} }
resolvedProfile, bridgeServerURL, acpBridgeServerProfiles := buildResolvedXWorkmateProfile(access, profile)
c.JSON(http.StatusOK, buildXWorkmateProfileResponse(access, profile, tokenConfigured)) c.JSON(http.StatusOK, buildXWorkmateProfileResponse(access, resolvedProfile, bridgeServerURL, acpBridgeServerProfiles, tokenConfigured))
} }
func (h *handler) getXWorkmateSecrets(c *gin.Context) { func (h *handler) getXWorkmateSecrets(c *gin.Context) {

View File

@ -220,9 +220,11 @@ func TestUpdateAndGetXWorkmateProfileRoundTripsSecretLocators(t *testing.T) {
Target string `json:"target"` Target string `json:"target"`
Required bool `json:"required"` Required bool `json:"required"`
} `json:"secretLocators"` } `json:"secretLocators"`
VaultSecretPath string `json:"vaultSecretPath"` VaultSecretPath string `json:"vaultSecretPath"`
VaultSecretKey string `json:"vaultSecretKey"` VaultSecretKey string `json:"vaultSecretKey"`
ApisixURL string `json:"apisixUrl"` ApisixURL string `json:"apisixUrl"`
BridgeServerURL string `json:"bridgeServerUrl"`
AcpBridgeServerProfiles []map[string]any `json:"acpBridgeServerProfiles"`
} `json:"profile"` } `json:"profile"`
TokenConfigured struct { TokenConfigured struct {
Openclaw bool `json:"openclaw"` Openclaw bool `json:"openclaw"`
@ -258,6 +260,87 @@ func TestUpdateAndGetXWorkmateProfileRoundTripsSecretLocators(t *testing.T) {
if resp.TokenConfigured.Apisix { if resp.TokenConfigured.Apisix {
t.Fatalf("expected apisix tokenConfigured=false without a token locator") t.Fatalf("expected apisix tokenConfigured=false without a token locator")
} }
if resp.Profile.BridgeServerURL != "" {
t.Fatalf("expected bridge server url to remain empty without shared defaults, got %q", resp.Profile.BridgeServerURL)
}
if len(resp.Profile.AcpBridgeServerProfiles) != 0 {
t.Fatalf("expected no acp bridge profiles without shared defaults, got %#v", resp.Profile.AcpBridgeServerProfiles)
}
}
func TestGetXWorkmateProfileUsesTenantSharedEnvDefaults(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Setenv("XWORKMATE_OPENCLAW_URL", "wss://openclaw.svc.plus:443")
t.Setenv("XWORKMATE_OPENCLAW_ORIGIN", "https://openclaw.svc.plus")
t.Setenv("XWORKMATE_VAULT_ADDR", "https://vault.svc.plus")
t.Setenv("XWORKMATE_VAULT_NAMESPACE", "shared")
t.Setenv("XWORKMATE_APISIX_URL", "https://api.svc.plus/v1")
t.Setenv("XWORKMATE_BRIDGE_SERVER_URL", "https://xworkmate-bridge.svc.plus")
t.Setenv("XWORKMATE_ACP_CODEX_URL", "wss://acp-server.svc.plus/codex")
t.Setenv("XWORKMATE_ACP_CODEX_AUTH_REF", "")
t.Setenv("XWORKMATE_ACP_OPENCODE_URL", "wss://acp-server.svc.plus/opencode")
t.Setenv("XWORKMATE_ACP_OPENCODE_AUTH_REF", "")
router, _, token := newXWorkmateTestHarness(t)
req := httptest.NewRequest(http.MethodGet, "/api/auth/xworkmate/profile", nil)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("X-Forwarded-Host", "console.svc.plus")
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected shared profile fetch success, got %d: %s", rec.Code, rec.Body.String())
}
var resp struct {
ProfileScope string `json:"profileScope"`
Profile struct {
OpenclawURL string `json:"openclawUrl"`
OpenclawOrigin string `json:"openclawOrigin"`
VaultURL string `json:"vaultUrl"`
VaultNamespace string `json:"vaultNamespace"`
ApisixURL string `json:"apisixUrl"`
BridgeServerURL string `json:"bridgeServerUrl"`
AcpBridgeServerProfiles []struct {
ProviderKey string `json:"providerKey"`
Endpoint string `json:"endpoint"`
} `json:"acpBridgeServerProfiles"`
} `json:"profile"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode shared profile response: %v", err)
}
if resp.ProfileScope != store.XWorkmateProfileScopeTenantShared {
t.Fatalf("expected tenant shared profile scope, got %q", resp.ProfileScope)
}
if resp.Profile.OpenclawURL != "wss://openclaw.svc.plus:443" {
t.Fatalf("expected shared openclaw url, got %q", resp.Profile.OpenclawURL)
}
if resp.Profile.OpenclawOrigin != "https://openclaw.svc.plus" {
t.Fatalf("expected shared openclaw origin, got %q", resp.Profile.OpenclawOrigin)
}
if resp.Profile.VaultURL != "https://vault.svc.plus" {
t.Fatalf("expected shared vault url, got %q", resp.Profile.VaultURL)
}
if resp.Profile.VaultNamespace != "shared" {
t.Fatalf("expected shared vault namespace, got %q", resp.Profile.VaultNamespace)
}
if resp.Profile.ApisixURL != "https://api.svc.plus/v1" {
t.Fatalf("expected shared apisix url, got %q", resp.Profile.ApisixURL)
}
if resp.Profile.BridgeServerURL != "https://xworkmate-bridge.svc.plus" {
t.Fatalf("expected shared bridge server url, got %q", resp.Profile.BridgeServerURL)
}
if len(resp.Profile.AcpBridgeServerProfiles) != 2 {
t.Fatalf("expected 2 shared acp bridge profiles, got %#v", resp.Profile.AcpBridgeServerProfiles)
}
if resp.Profile.AcpBridgeServerProfiles[0].ProviderKey != "codex" || resp.Profile.AcpBridgeServerProfiles[0].Endpoint != "wss://acp-server.svc.plus/codex" {
t.Fatalf("expected codex shared profile, got %#v", resp.Profile.AcpBridgeServerProfiles[0])
}
if resp.Profile.AcpBridgeServerProfiles[1].ProviderKey != "opencode" || resp.Profile.AcpBridgeServerProfiles[1].Endpoint != "wss://acp-server.svc.plus/opencode" {
t.Fatalf("expected opencode shared profile, got %#v", resp.Profile.AcpBridgeServerProfiles[1])
}
} }
func TestUpdateXWorkmateProfileSynthesizesSecretLocatorsFromLegacyFields(t *testing.T) { func TestUpdateXWorkmateProfileSynthesizesSecretLocatorsFromLegacyFields(t *testing.T) {