refactor(acp): remove hardcoded endpoints and migrate to path-based routing
This commit is contained in:
parent
55bfda225e
commit
939a1b6c87
@ -2,10 +2,10 @@
|
||||
|
||||
This document records the post-deployment validation of the bridge public
|
||||
origin at `xworkmate-bridge.svc.plus` and the independent upstream ACP ingress
|
||||
at `acp-server.svc.plus`.
|
||||
at `xworkmate-bridge.svc.plus/acp-server`.
|
||||
|
||||
For APP integration, the canonical public contract remains the bridge origin
|
||||
and the `.../acp/rpc` path on that origin. The direct `acp-server.svc.plus`
|
||||
and the `.../acp/rpc` path on that origin. The direct `xworkmate-bridge.svc.plus/acp-server`
|
||||
URLs in this document are upstream validation targets, not the preferred APP
|
||||
entry points.
|
||||
|
||||
@ -29,9 +29,9 @@ Recommended APP-facing endpoint:
|
||||
|
||||
Verified public HTTP JSON-RPC endpoints:
|
||||
|
||||
- Codex: `https://acp-server.svc.plus/codex/acp/rpc`
|
||||
- OpenCode: `https://acp-server.svc.plus/opencode/acp/rpc`
|
||||
- Gemini: `https://acp-server.svc.plus/gemini/acp/rpc`
|
||||
- Codex: `https://xworkmate-bridge.svc.plus/acp-server/codex/acp/rpc`
|
||||
- OpenCode: `https://xworkmate-bridge.svc.plus/acp-server/opencode/acp/rpc`
|
||||
- Gemini: `https://xworkmate-bridge.svc.plus/acp-server/gemini/acp/rpc`
|
||||
|
||||
The `.../acp` path remains reserved for WebSocket ACP.
|
||||
|
||||
|
||||
@ -55,9 +55,9 @@ flowchart TD
|
||||
end
|
||||
|
||||
subgraph UPSTREAM["Independent upstream services"]
|
||||
C1["https://acp-server.svc.plus/codex/acp/rpc"]
|
||||
C2["https://acp-server.svc.plus/opencode/acp/rpc"]
|
||||
C3["https://acp-server.svc.plus/gemini/acp/rpc"]
|
||||
C1["https://xworkmate-bridge.svc.plus/acp-server/codex/acp/rpc"]
|
||||
C2["https://xworkmate-bridge.svc.plus/acp-server/opencode/acp/rpc"]
|
||||
C3["https://xworkmate-bridge.svc.plus/acp-server/gemini/acp/rpc"]
|
||||
C4["wss://openclaw.svc.plus"]
|
||||
end
|
||||
|
||||
@ -107,9 +107,9 @@ flowchart LR
|
||||
end
|
||||
|
||||
subgraph L3["上游视角"]
|
||||
U1["https://acp-server.svc.plus/codex/acp/rpc"]
|
||||
U2["https://acp-server.svc.plus/opencode/acp/rpc"]
|
||||
U3["https://acp-server.svc.plus/gemini/acp/rpc"]
|
||||
U1["https://xworkmate-bridge.svc.plus/acp-server/codex/acp/rpc"]
|
||||
U2["https://xworkmate-bridge.svc.plus/acp-server/opencode/acp/rpc"]
|
||||
U3["https://xworkmate-bridge.svc.plus/acp-server/gemini/acp/rpc"]
|
||||
U4["wss://openclaw.svc.plus<br/>reported as openclaw.svc.plus:443"]
|
||||
end
|
||||
|
||||
@ -163,7 +163,7 @@ Important distinction:
|
||||
## Invariants
|
||||
|
||||
- app traffic reaches upstream ACP and gateway services only through the bridge
|
||||
- app does not call `acp-server.svc.plus/*` or `openclaw.svc.plus` directly
|
||||
- app does not call `xworkmate-bridge.svc.plus/acp-server/*` or `openclaw.svc.plus` directly
|
||||
- upstream auth stays bridge-internal:
|
||||
- `Authorization: Bearer $INTERNAL_SERVICE_TOKEN`
|
||||
- `acp.capabilities` is the provider / capability discovery source
|
||||
|
||||
@ -127,7 +127,7 @@ Use these terms consistently:
|
||||
|
||||
- `canonical app-facing path`: `/acp/rpc` and `/acp`
|
||||
- `gateway runtime method family`: `xworkmate.gateway.*`
|
||||
- `independent upstream service`: `acp-server.svc.plus/*`, `wss://openclaw.svc.plus`
|
||||
- `independent upstream service`: `xworkmate-bridge.svc.plus/acp-server/*`, `wss://openclaw.svc.plus`
|
||||
- `bridge-owned routing`: provider / gateway selection performed inside bridge
|
||||
- `routing metadata`: execution target and resolved provider/gateway identifiers returned to the app
|
||||
|
||||
|
||||
@ -1,8 +1,49 @@
|
||||
package acp
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResolveSingleAgentForwardEndpoint(t *testing.T) {
|
||||
func TestResolveSingleAgentForwardEndpointFromExampleConfig(t *testing.T) {
|
||||
// Set the config path to example/config.yaml relative to this test file
|
||||
os.Setenv("BRIDGE_CONFIG_PATH", "../../example/config.yaml")
|
||||
defer os.Unsetenv("BRIDGE_CONFIG_PATH")
|
||||
|
||||
catalog, order := newProductionProviderCatalog()
|
||||
if len(order) == 0 {
|
||||
t.Fatal("Expected non-empty provider order from example/config.yaml")
|
||||
}
|
||||
|
||||
expectedEndpoints := map[string]string{
|
||||
"codex": "https://xworkmate-bridge.svc.plus/acp-server/codex/acp/rpc",
|
||||
"opencode": "https://xworkmate-bridge.svc.plus/acp-server/opencode/acp/rpc",
|
||||
"gemini": "https://xworkmate-bridge.svc.plus/acp-server/gemini/acp/rpc",
|
||||
}
|
||||
|
||||
for _, id := range order {
|
||||
id := id
|
||||
t.Run(id, func(t *testing.T) {
|
||||
provider, ok := catalog[id]
|
||||
if !ok {
|
||||
t.Errorf("Provider %s missing from catalog", id)
|
||||
return
|
||||
}
|
||||
if !provider.Enabled {
|
||||
t.Errorf("Provider %s should be enabled in example config", id)
|
||||
}
|
||||
|
||||
want := expectedEndpoints[id]
|
||||
got := resolveSingleAgentForwardEndpoint(provider)
|
||||
if got != want {
|
||||
t.Errorf("resolveSingleAgentForwardEndpoint(%s) = %q, want %q (from example config)", id, got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveSingleAgentForwardEndpointManual(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
@ -13,26 +54,10 @@ func TestResolveSingleAgentForwardEndpoint(t *testing.T) {
|
||||
{
|
||||
name: "preserves upstream endpoint",
|
||||
provider: syncedProvider{
|
||||
ProviderID: "opencode",
|
||||
Endpoint: "https://acp-server.svc.plus/opencode/acp/rpc",
|
||||
ProviderID: "custom",
|
||||
Endpoint: "https://upstream-provider.example.com/acp/rpc",
|
||||
},
|
||||
want: "https://acp-server.svc.plus/opencode/acp/rpc",
|
||||
},
|
||||
{
|
||||
name: "does not rewrite bridge endpoint placeholder for codex",
|
||||
provider: syncedProvider{
|
||||
ProviderID: "codex",
|
||||
Endpoint: "https://xworkmate-bridge.svc.plus",
|
||||
},
|
||||
want: "https://xworkmate-bridge.svc.plus",
|
||||
},
|
||||
{
|
||||
name: "does not rewrite bridge endpoint placeholder for gemini",
|
||||
provider: syncedProvider{
|
||||
ProviderID: "gemini",
|
||||
Endpoint: "https://xworkmate-bridge.svc.plus",
|
||||
},
|
||||
want: "https://xworkmate-bridge.svc.plus",
|
||||
want: "https://upstream-provider.example.com/acp/rpc",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -8,14 +8,6 @@ import (
|
||||
"xworkmate-bridge/internal/shared"
|
||||
)
|
||||
|
||||
// 默认生产端点
|
||||
const (
|
||||
productionGatewayEndpointURL = "https://xworkmate-bridge.svc.plus/gateway/openclaw/"
|
||||
productionCodexEndpointURL = "https://xworkmate-bridge.svc.plus/acp-server/codex/acp/rpc"
|
||||
productionOpenCodeEndpointURL = "https://xworkmate-bridge.svc.plus/acp-server/opencode/acp/rpc"
|
||||
productionGeminiEndpointURL = "https://xworkmate-bridge.svc.plus/acp-server/gemini/acp/rpc"
|
||||
)
|
||||
|
||||
type syncedProvider struct {
|
||||
ProviderID string
|
||||
Label string
|
||||
@ -46,17 +38,15 @@ func loadBridgeConfig() *BridgeConfig {
|
||||
return config
|
||||
}
|
||||
|
||||
func resolveURL(yamlVal, envKey, defaultVal string) string {
|
||||
func resolveURL(yamlVal, envKey string) string {
|
||||
val := strings.TrimSpace(yamlVal)
|
||||
if val != "" {
|
||||
return val
|
||||
}
|
||||
return strings.TrimSpace(shared.EnvOrDefault(envKey, defaultVal))
|
||||
return strings.TrimSpace(shared.EnvOrDefault(envKey, ""))
|
||||
}
|
||||
|
||||
func bridgeUpstreamAuthorizationHeader() string {
|
||||
// Original logic used firstNonEmptyString and normalizeAuthorizationHeader
|
||||
// but let's keep it simple and match expected "Bearer token" if it exists.
|
||||
token := strings.TrimSpace(shared.EnvOrDefault("BRIDGE_AUTH_TOKEN", ""))
|
||||
if token == "" {
|
||||
token = strings.TrimSpace(shared.EnvOrDefault("INTERNAL_SERVICE_TOKEN", ""))
|
||||
@ -71,30 +61,32 @@ func newProductionProviderCatalog() (map[string]syncedProvider, []string) {
|
||||
config := loadBridgeConfig()
|
||||
authorizationHeader := bridgeUpstreamAuthorizationHeader()
|
||||
|
||||
catalog := map[string]syncedProvider{
|
||||
"codex": {
|
||||
ProviderID: "codex",
|
||||
Label: "Codex",
|
||||
Endpoint: resolveURL(config.Upstream.CodexURL, "OPENCLAW_CODEX_URL", productionCodexEndpointURL),
|
||||
AuthorizationHeader: authorizationHeader,
|
||||
Enabled: true,
|
||||
},
|
||||
"opencode": {
|
||||
ProviderID: "opencode",
|
||||
Label: "OpenCode",
|
||||
Endpoint: resolveURL(config.Upstream.OpenCodeURL, "OPENCLAW_OPENCODE_URL", productionOpenCodeEndpointURL),
|
||||
AuthorizationHeader: authorizationHeader,
|
||||
Enabled: true,
|
||||
},
|
||||
"gemini": {
|
||||
ProviderID: "gemini",
|
||||
Label: "Gemini",
|
||||
Endpoint: resolveURL(config.Upstream.GeminiURL, "OPENCLAW_GEMINI_URL", productionGeminiEndpointURL),
|
||||
AuthorizationHeader: authorizationHeader,
|
||||
Enabled: true,
|
||||
},
|
||||
providers := []struct {
|
||||
id string
|
||||
label string
|
||||
yaml string
|
||||
envKey string
|
||||
}{
|
||||
{"codex", "Codex", config.Upstream.CodexURL, "OPENCLAW_CODEX_URL"},
|
||||
{"opencode", "OpenCode", config.Upstream.OpenCodeURL, "OPENCLAW_OPENCODE_URL"},
|
||||
{"gemini", "Gemini", config.Upstream.GeminiURL, "OPENCLAW_GEMINI_URL"},
|
||||
}
|
||||
order := []string{"codex", "opencode", "gemini"}
|
||||
|
||||
catalog := make(map[string]syncedProvider)
|
||||
var order []string
|
||||
|
||||
for _, p := range providers {
|
||||
endpoint := resolveURL(p.yaml, p.envKey)
|
||||
catalog[p.id] = syncedProvider{
|
||||
ProviderID: p.id,
|
||||
Label: p.label,
|
||||
Endpoint: endpoint,
|
||||
AuthorizationHeader: authorizationHeader,
|
||||
Enabled: endpoint != "",
|
||||
}
|
||||
order = append(order, p.id)
|
||||
}
|
||||
|
||||
return catalog, order
|
||||
}
|
||||
|
||||
@ -108,7 +100,7 @@ func (s *Server) syncedProviderByID(providerID string) (syncedProvider, bool) {
|
||||
func (s *Server) availableProviderCatalog() []Provider {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
|
||||
var catalog []Provider
|
||||
for _, id := range s.providerOrder {
|
||||
if p, ok := s.providerCatalog[id]; ok && p.Enabled {
|
||||
@ -125,7 +117,7 @@ func (s *Server) availableProviderCatalog() []Provider {
|
||||
func (s *Server) availableProviders() []string {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
|
||||
var providers []string
|
||||
for _, id := range s.providerOrder {
|
||||
if p, ok := s.providerCatalog[id]; ok && p.Enabled {
|
||||
|
||||
@ -109,7 +109,7 @@ case "${scenario}" in
|
||||
https://xworkmate-bridge.svc.plus/)
|
||||
printf 'xworkmate-bridge is running\n'
|
||||
;;
|
||||
https://acp-server.svc.plus/*/acp/rpc)
|
||||
https://xworkmate-bridge.svc.plus/acp-server/*/acp/rpc)
|
||||
printf '{"jsonrpc":"2.0","result":{"providers":["ok"]}}\n'
|
||||
;;
|
||||
https://xworkmate-bridge.svc.plus/acp/rpc)
|
||||
@ -135,7 +135,7 @@ case "${scenario}" in
|
||||
https://xworkmate-bridge.svc.plus/)
|
||||
printf 'xworkmate-bridge is running\n'
|
||||
;;
|
||||
https://acp-server.svc.plus/*/acp/rpc)
|
||||
https://xworkmate-bridge.svc.plus/acp-server/*/acp/rpc)
|
||||
printf '{"jsonrpc":"2.0","result":{"providers":["ok"]}}\n'
|
||||
;;
|
||||
https://xworkmate-bridge.svc.plus/acp/rpc)
|
||||
@ -191,9 +191,9 @@ run_validate_capture() {
|
||||
FAKE_CURL_STATE_DIR="${RUN_STATE_DIR}" \
|
||||
BRIDGE_SERVER_URL="https://xworkmate-bridge.svc.plus" \
|
||||
OPENCLAW_URL="wss://openclaw.svc.plus" \
|
||||
CODEX_RPC_URL="https://acp-server.svc.plus/codex/acp/rpc" \
|
||||
OPENCODE_RPC_URL="https://acp-server.svc.plus/opencode/acp/rpc" \
|
||||
GEMINI_RPC_URL="https://acp-server.svc.plus/gemini/acp/rpc" \
|
||||
CODEX_RPC_URL="https://xworkmate-bridge.svc.plus/acp-server/codex/acp/rpc" \
|
||||
OPENCODE_RPC_URL="https://xworkmate-bridge.svc.plus/acp-server/opencode/acp/rpc" \
|
||||
GEMINI_RPC_URL="https://xworkmate-bridge.svc.plus/acp-server/gemini/acp/rpc" \
|
||||
INTERNAL_SERVICE_TOKEN="test-token" \
|
||||
bash "${SCRIPT_PATH}" "${IMAGE_REF}" 2>&1
|
||||
)"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user