Add shared XWorkmate bridge defaults

This commit is contained in:
Haitao Pan 2026-04-09 10:30:26 +08:00
parent 70c6a3f82f
commit d86463ef8a
2 changed files with 183 additions and 22 deletions

View File

@ -8,6 +8,7 @@ import (
"fmt"
"net/http"
"net/url"
"os"
"strings"
"github.com/gin-gonic/gin"
@ -26,14 +27,16 @@ type xworkmateAccessContext struct {
}
type xworkmateProfilePayload struct {
OpenclawURL string `json:"openclawUrl"`
OpenclawOrigin string `json:"openclawOrigin"`
VaultURL string `json:"vaultUrl"`
VaultNamespace string `json:"vaultNamespace"`
VaultSecretPath string `json:"vaultSecretPath"`
VaultSecretKey string `json:"vaultSecretKey"`
SecretLocators []xworkmateSecretLocatorPayload `json:"secretLocators"`
ApisixURL string `json:"apisixUrl"`
OpenclawURL string `json:"openclawUrl"`
OpenclawOrigin string `json:"openclawOrigin"`
VaultURL string `json:"vaultUrl"`
VaultNamespace string `json:"vaultNamespace"`
VaultSecretPath string `json:"vaultSecretPath"`
VaultSecretKey string `json:"vaultSecretKey"`
SecretLocators []xworkmateSecretLocatorPayload `json:"secretLocators"`
ApisixURL string `json:"apisixUrl"`
BridgeServerURL string `json:"bridgeServerUrl"`
AcpBridgeServerProfiles []xworkmateAcpBridgeServerProfilePayload `json:"acpBridgeServerProfiles"`
}
type xworkmateSecretLocatorPayload struct {
@ -45,6 +48,15 @@ type xworkmateSecretLocatorPayload struct {
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{}{
"openclawtoken": {},
"gatewaytoken": {},
@ -365,16 +377,80 @@ func (h *handler) buildSessionUser(ctx context.Context, host string, user *store
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{
"openclawUrl": "",
"openclawOrigin": "",
"vaultUrl": "",
"vaultNamespace": "",
"vaultSecretPath": "",
"vaultSecretKey": "",
"secretLocators": []gin.H{},
"apisixUrl": "",
"openclawUrl": "",
"openclawOrigin": "",
"vaultUrl": "",
"vaultNamespace": "",
"vaultSecretPath": "",
"vaultSecretKey": "",
"secretLocators": []gin.H{},
"apisixUrl": "",
"bridgeServerUrl": bridgeServerURL,
"acpBridgeServerProfiles": acpBridgeServerProfiles,
}
if profile != nil {
resolvedProfile["openclawUrl"] = profile.OpenclawURL
@ -615,8 +691,9 @@ func (h *handler) getXWorkmateProfile(c *gin.Context) {
}
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) {
@ -705,8 +782,9 @@ func (h *handler) updateXWorkmateProfile(c *gin.Context) {
}
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) {

View File

@ -220,9 +220,11 @@ func TestUpdateAndGetXWorkmateProfileRoundTripsSecretLocators(t *testing.T) {
Target string `json:"target"`
Required bool `json:"required"`
} `json:"secretLocators"`
VaultSecretPath string `json:"vaultSecretPath"`
VaultSecretKey string `json:"vaultSecretKey"`
ApisixURL string `json:"apisixUrl"`
VaultSecretPath string `json:"vaultSecretPath"`
VaultSecretKey string `json:"vaultSecretKey"`
ApisixURL string `json:"apisixUrl"`
BridgeServerURL string `json:"bridgeServerUrl"`
AcpBridgeServerProfiles []map[string]any `json:"acpBridgeServerProfiles"`
} `json:"profile"`
TokenConfigured struct {
Openclaw bool `json:"openclaw"`
@ -258,6 +260,87 @@ func TestUpdateAndGetXWorkmateProfileRoundTripsSecretLocators(t *testing.T) {
if resp.TokenConfigured.Apisix {
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) {