Compare commits
1 Commits
main
...
codex/xwor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d86463ef8a |
116
api/xworkmate.go
116
api/xworkmate.go
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user