Make production provider routing bridge-owned

This commit is contained in:
Haitao Pan 2026-04-11 13:50:11 +08:00
parent ca66dee7b3
commit d1bec8d253
12 changed files with 237 additions and 527 deletions

View File

@ -86,7 +86,6 @@ The bridge currently handles these methods:
| `session.close` | Close session state |
| `xworkmate.dispatch.resolve` | Resolve provider choice from candidate providers and requirements |
| `xworkmate.routing.resolve` | Resolve execution target / provider / skills from routing metadata |
| `xworkmate.providers.sync` | Sync external single-agent provider catalog into the bridge |
| `xworkmate.mounts.reconcile` | Reconcile managed MCP configuration and mount-related settings |
| `xworkmate.gateway.connect` | Connect bridge runtime to gateway |
| `xworkmate.gateway.request` | Send a request to the connected gateway runtime |
@ -119,16 +118,16 @@ Response shape:
"multiAgent": true,
"providerCatalog": [
{ "providerId": "codex", "label": "Codex" },
{ "providerId": "gemini", "label": "Gemini" },
{ "providerId": "opencode", "label": "OpenCode" }
{ "providerId": "opencode", "label": "OpenCode" },
{ "providerId": "gemini", "label": "Gemini" }
],
"capabilities": {
"single_agent": true,
"multi_agent": true,
"providerCatalog": [
{ "providerId": "codex", "label": "Codex" },
{ "providerId": "gemini", "label": "Gemini" },
{ "providerId": "opencode", "label": "OpenCode" }
{ "providerId": "opencode", "label": "OpenCode" },
{ "providerId": "gemini", "label": "Gemini" }
]
}
}
@ -137,9 +136,12 @@ Response shape:
Notes:
- `providerCatalog` comes from the synced external provider catalog registered
through `xworkmate.providers.sync`
- provider order is bridge-owned and preserves the sync order
- `providerCatalog` is bridge-owned and built in at startup
- production provider map is fixed to:
- `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`
- upstream ACP auth uses `Authorization: Bearer $INTERNAL_SERVICE_TOKEN`
- `multiAgent` is controlled by `ACP_MULTI_AGENT_ENABLED`, default `true`
### 3.2 `session.start`
@ -343,58 +345,7 @@ Representative response fields:
- `skillCandidates`
- `memorySources`
### 3.8 `xworkmate.providers.sync`
Purpose:
- register external ACP single-agent providers into the bridge
Request example:
```json
{
"jsonrpc": "2.0",
"id": "sync-1",
"method": "xworkmate.providers.sync",
"params": {
"providers": [
{
"providerId": "opencode",
"label": "OpenCode",
"endpoint": "https://acp-server.svc.plus/opencode/acp/rpc",
"authorizationHeader": "Bearer ${OPENCODE_AUTH_TOKEN}",
"enabled": true
}
]
}
}
```
Provider fields:
- `providerId`
- `label`
- `endpoint`
- `authorizationHeader`
- `enabled`
Response shape:
```json
{
"ok": true,
"providers": [
{
"providerId": "opencode",
"label": "OpenCode",
"endpoint": "https://acp-server.svc.plus/opencode/acp/rpc",
"enabled": true
}
]
}
```
### 3.9 `xworkmate.mounts.reconcile`
### 3.8 `xworkmate.mounts.reconcile`
Purpose:
@ -423,13 +374,13 @@ Managed MCP server item shape:
}
```
### 3.10 Gateway runtime methods
### 3.9 Gateway runtime methods
#### `xworkmate.gateway.connect`
Purpose:
- connect a bridge runtime session to the gateway runtime
- connect a bridge runtime session to the bridge-owned production gateway route
Key params:
@ -459,6 +410,13 @@ Response fields:
- `returnedDeviceToken`
- `error`
Notes:
- for `mode=remote`, the bridge overrides runtime endpoint selection to
`wss://openclaw.svc.plus`
- upstream gateway auth uses `Authorization: Bearer $INTERNAL_SERVICE_TOKEN`
- the app does not provide production openclaw endpoint truth
#### `xworkmate.gateway.request`
Purpose:

View File

@ -1,55 +1,50 @@
# ACP Forwarding Topology
This document describes how `xworkmate-bridge.svc.plus` forwards requests to the public ACP and gateway endpoints.
This document describes the bridge-only production forwarding model for `xworkmate-bridge.svc.plus`.
## Topology
```mermaid
flowchart TD
U[Client / App] --> B[xworkmate-bridge.svc.plus]
U["xworkmate-app"] --> B["https://xworkmate-bridge.svc.plus"]
B -->|HTTP POST /acp/rpc| ACPRPC[ACP HTTP RPC handler]
B -->|WebSocket /acp| ACPWS[ACP WebSocket handler]
B -->|POST /acp/rpc| RPC["ACP RPC handler"]
B -->|WS /acp| WS["ACP WebSocket handler"]
ACPRPC --> R{Method?}
ACPWS --> R
RPC --> R{"method"}
WS --> R
R -->|acp.capabilities| CAP[Return available provider list]
R -->|session.start / session.message| ENQ[Resolve routing and enqueue turn]
R -->|session.cancel / session.close| LIFE[Session lifecycle control]
R -->|xworkmate.providers.sync| SYNC[Sync external provider catalog]
R -->|xworkmate.gateway.*| GWAPI[Gateway control methods]
R -->|xworkmate.dispatch.resolve| DISPATCH[Dispatch resolution]
R -->|xworkmate.routing.resolve| ROUTE[Routing resolution]
R -->|acp.capabilities| CAP["built-in provider catalog"]
R -->|xworkmate.routing.resolve| ROUTE["bridge-owned routing resolve"]
R -->|session.start / session.message| RUN["bridge-owned execution"]
R -->|xworkmate.gateway.*| GWAPI["gateway runtime proxy"]
R -->|session.cancel / session.close| LIFE["session lifecycle"]
ENQ --> D{Resolved execution target}
D -->|gateway / openclaw| GW[gatewayruntime.Manager]
D -->|singleAgent + codex| C[codex provider]
D -->|singleAgent + opencode| O[opencode provider]
D -->|singleAgent + gemini| G[gemini provider]
RUN --> ACP1["codex -> https://acp-server.svc.plus/codex/acp/rpc"]
RUN --> ACP2["opencode -> https://acp-server.svc.plus/opencode/acp/rpc"]
RUN --> ACP3["gemini -> https://acp-server.svc.plus/gemini/acp/rpc"]
GW --> OCLAW[wss://openclaw.svc.plus]
C --> CODR[https://acp-server.svc.plus/codex/acp/rpc]
O --> OPR[https://acp-server.svc.plus/opencode/acp/rpc]
G --> GMR[https://acp-server.svc.plus/gemini/acp/rpc]
SYNC --> CAT[providerCatalog]
CAT --> C
CAT --> O
CAT --> G
GWAPI --> GW["wss://openclaw.svc.plus"]
```
## Request Flow
## Production Truth
The bridge accepts ACP JSON-RPC over `POST /acp/rpc` and ACP WebSocket traffic over `/acp`.
Bridge owns the production map:
For `session.start` and `session.message`, the server resolves routing metadata, selects either the gateway runtime or a single-agent provider, and then forwards the turn to the resolved endpoint.
- `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`
- gateway -> `wss://openclaw.svc.plus`
For the public single-agent ACP providers, `http` and `https` endpoints are forwarded as JSON-RPC `POST .../acp/rpc` requests, while `ws` and `wss` endpoints are forwarded as WebSocket ACP sessions on `/acp`.
Upstream auth is bridge-internal:
## Current Public Endpoints
- `Authorization: Bearer $INTERNAL_SERVICE_TOKEN`
- `wss://openclaw.svc.plus`
- `https://acp-server.svc.plus/codex/acp/rpc`
- `https://acp-server.svc.plus/opencode/acp/rpc`
- `https://acp-server.svc.plus/gemini/acp/rpc`
## Invariants
- app-facing cloud entry is only `https://xworkmate-bridge.svc.plus`
- `acp.capabilities` returns the built-in production catalog
- no production `xworkmate.providers.sync`
- no app direct call to `acp-server.svc.plus/*`
- no app direct call to `openclaw.svc.plus`
- remote gateway runtime status is reported as `openclaw.svc.plus:443`, but the app still talks only to the bridge

View File

@ -103,14 +103,16 @@ xworkmate app / agent manager
-> stdio child process: gemini --experimental-acp
```
The adapter should be registered in bridge provider sync as an external ACP provider:
The adapter should be registered in the bridge-owned provider catalog as the
`gemini` single-agent ACP backend:
- `providerId: "gemini"`
- `label: "Gemini"`
- `endpoint: http://127.0.0.1:<port>/acp/rpc` or `ws://127.0.0.1:<port>/acp`
- `enabled: true`
`xworkmate-bridge` already supports this provider shape through `xworkmate.providers.sync`.
In production, `xworkmate-bridge` exposes `gemini` from its built-in provider
catalog rather than from app-driven sync.
## Adapter Responsibilities
@ -302,25 +304,17 @@ If Gemini ACP later gains a compatible conversation method, you can override the
export GEMINI_ADAPTER_UPSTREAM_METHOD=your-discovered-gemini-method
```
## Bridge Provider Sync Example
## Bridge Provider Catalog Example
Once the adapter is running, register it as a normal external provider:
Once the adapter is running, expose it from the bridge-owned provider catalog
as the `gemini` backend:
```json
{
"jsonrpc": "2.0",
"id": "providers-sync-1",
"method": "xworkmate.providers.sync",
"params": {
"providers": [
{
"providerId": "gemini",
"label": "Gemini",
"endpoint": "http://127.0.0.1:8791/acp/rpc",
"enabled": true
}
]
}
"providerId": "gemini",
"label": "Gemini",
"endpoint": "http://127.0.0.1:8791/acp/rpc",
"enabled": true
}
```
@ -333,23 +327,15 @@ Example local startup and sync flow:
--gemini-args="--experimental-acp"
```
Then sync this provider into the bridge:
Then wire the bridge startup/runtime config so the built-in `gemini` catalog
entry points at this adapter endpoint:
```json
{
"jsonrpc": "2.0",
"id": "providers-sync-gemini",
"method": "xworkmate.providers.sync",
"params": {
"providers": [
{
"providerId": "gemini",
"label": "Gemini",
"endpoint": "http://127.0.0.1:8791",
"enabled": true
}
]
}
"providerId": "gemini",
"label": "Gemini",
"endpoint": "http://127.0.0.1:8791",
"enabled": true
}
```

View File

@ -19,7 +19,7 @@
- `resolvedExecutionTarget`
- `resolvedProviderId`
- `resolvedEndpointTarget`
- `xworkmate.providers.sync` 能把 `accounts.svc.plus` 同步来的外部 provider 注入 bridge,并参与后续路由选择。
- `acp.capabilities` 暴露 bridge 内建的生产 provider catalog,并参与后续路由选择。
### 2. 典型 Case 层
@ -96,7 +96,7 @@ flutter test test/runtime/app_controller_single_agent_workspace_binding_regressi
### 路由发现层断言
- `acp.capabilities` 的 provider 列表来自 bridge 当前环境,而不是本地写死。
- `xworkmate.providers.sync` 后,新增 provider 能进入能力面与路由面
- bridge 内建生产 catalog 包含 `codex / opencode / gemini`,且不依赖 app 侧预同步
- `xworkmate.routing.resolve` 在 skill / prompt / target 组合下,返回合理的 provider 与 endpoint target。
### 执行层断言

View File

@ -21,10 +21,7 @@ import (
)
const (
externalProviderEndpointKey = "externalProviderEndpoint"
externalProviderAuthorizationHeaderKey = "externalProviderAuthorizationHeader"
externalProviderLabelKey = "externalProviderLabel"
inboundAuthorizationHeaderKey = "bridgeAuthorizationHeader"
inboundAuthorizationHeaderKey = "bridgeAuthorizationHeader"
)
func buildResolvedExecutionParams(
@ -61,25 +58,6 @@ func buildResolvedExecutionParams(
return next
}
func injectResolvedExternalProviderParams(
params map[string]any,
provider syncedProvider,
) map[string]any {
if params == nil {
params = map[string]any{}
}
if endpoint := strings.TrimSpace(provider.Endpoint); endpoint != "" {
params[externalProviderEndpointKey] = endpoint
}
if authorization := strings.TrimSpace(provider.AuthorizationHeader); authorization != "" {
params[externalProviderAuthorizationHeaderKey] = authorization
}
if label := strings.TrimSpace(provider.Label); label != "" {
params[externalProviderLabelKey] = label
}
return params
}
func injectInboundAuthorizationHeader(params map[string]any, authorization string) map[string]any {
if params == nil {
params = map[string]any{}
@ -184,18 +162,7 @@ func (s *Server) runSingleAgentViaExternalProvider(
}
func resolveSingleAgentForwardEndpoint(provider syncedProvider) string {
endpoint := strings.TrimSpace(provider.Endpoint)
if endpoint == "" {
return ""
}
if !strings.Contains(strings.ToLower(endpoint), "xworkmate-bridge.svc.plus") {
return endpoint
}
providerID := strings.TrimSpace(strings.ToLower(provider.ProviderID))
if providerID == "" {
return endpoint
}
return fmt.Sprintf("https://acp-server.svc.plus/%s/acp/rpc", providerID)
return strings.TrimSpace(provider.Endpoint)
}
func sanitizeExternalACPParams(method string, params map[string]any) map[string]any {
@ -213,9 +180,6 @@ func sanitizeExternalACPParams(method string, params map[string]any) map[string]
delete(next, "resolvedProviderId")
delete(next, "resolvedModel")
delete(next, "resolvedSkills")
delete(next, externalProviderEndpointKey)
delete(next, externalProviderAuthorizationHeaderKey)
delete(next, externalProviderLabelKey)
delete(next, inboundAuthorizationHeaderKey)
// Gateway-only fields are irrelevant in ACP single-agent forwarding.
normalizedMethod := strings.TrimSpace(method)
@ -226,22 +190,6 @@ func sanitizeExternalACPParams(method string, params map[string]any) map[string]
return next
}
func externalProviderFromParams(params map[string]any) (syncedProvider, bool) {
endpoint := strings.TrimSpace(shared.StringArg(params, externalProviderEndpointKey, ""))
if endpoint == "" {
return syncedProvider{}, false
}
return syncedProvider{
ProviderID: strings.TrimSpace(shared.StringArg(params, "provider", "")),
Label: strings.TrimSpace(shared.StringArg(params, externalProviderLabelKey, "")),
Endpoint: endpoint,
AuthorizationHeader: strings.TrimSpace(
shared.StringArg(params, externalProviderAuthorizationHeaderKey, ""),
),
Enabled: true,
}, true
}
func requestExternalACP(
ctx context.Context,
endpoint,

View File

@ -19,20 +19,20 @@ func TestResolveSingleAgentForwardEndpoint(t *testing.T) {
want: "https://acp-server.svc.plus/opencode/acp/rpc",
},
{
name: "rewrites bridge discovery endpoint to codex upstream",
name: "does not rewrite bridge endpoint placeholder for codex",
provider: syncedProvider{
ProviderID: "codex",
Endpoint: "https://xworkmate-bridge.svc.plus",
},
want: "https://acp-server.svc.plus/codex/acp/rpc",
want: "https://xworkmate-bridge.svc.plus",
},
{
name: "rewrites bridge discovery endpoint to gemini upstream",
name: "does not rewrite bridge endpoint placeholder for gemini",
provider: syncedProvider{
ProviderID: "gemini",
Endpoint: "https://xworkmate-bridge.svc.plus",
},
want: "https://acp-server.svc.plus/gemini/acp/rpc",
want: "https://xworkmate-bridge.svc.plus",
},
}

View File

@ -53,6 +53,7 @@ func handleGatewayConnect(
Password: strings.TrimSpace(shared.StringArg(asMap(params["auth"]), "password", "")),
},
}
request = applyProductionGatewayRouting(request)
request.ReportedRemoteAddress = resolveGatewayReportedRemoteAddress(server, request)
result := server.gateway.Connect(request, notify)
return map[string]any{
@ -64,6 +65,28 @@ func handleGatewayConnect(
}
}
func applyProductionGatewayRouting(
request gatewayruntime.ConnectRequest,
) gatewayruntime.ConnectRequest {
if strings.TrimSpace(strings.ToLower(request.Mode)) != "remote" {
return request
}
request.Endpoint = gatewayruntime.Endpoint{
Host: "openclaw.svc.plus",
Port: 443,
TLS: true,
}
request.Auth.Token = strings.TrimSpace(
shared.EnvOrDefault("INTERNAL_SERVICE_TOKEN", ""),
)
request.Auth.Password = ""
request.ConnectAuthMode = "shared-token"
request.ConnectAuthFields = []string{"token"}
request.ConnectAuthSources = []string{"bridge"}
request.HasSharedAuth = request.Auth.Token != ""
return request
}
func handleGatewayRequest(
server *Server,
params map[string]any,
@ -167,28 +190,8 @@ func resolveGatewayReportedRemoteAddress(
if strings.TrimSpace(strings.ToLower(request.Mode)) != "remote" {
return ""
}
if !shouldOverrideGatewayReportedRemoteAddress(request.Endpoint.Host) {
return ""
}
if server != nil {
if provider, ok := server.syncedProviderByID("openclaw"); ok {
if reported := publicEndpointAddressLabel(provider.Endpoint); reported != "" {
return reported
}
}
}
return publicEndpointAddressLabel(
shared.EnvOrDefault("OPENCLAW_URL", "wss://openclaw.svc.plus"),
)
}
func shouldOverrideGatewayReportedRemoteAddress(host string) bool {
switch strings.TrimSpace(strings.ToLower(host)) {
case "127.0.0.1", "localhost", "::1", "xworkmate-bridge.svc.plus":
return true
default:
return false
}
_ = server
return publicEndpointAddressLabel(productionGatewayEndpointURL)
}
func publicEndpointAddressLabel(raw string) string {

View File

@ -6,18 +6,10 @@ import (
"xworkmate-bridge/internal/gatewayruntime"
)
func TestResolveGatewayReportedRemoteAddressUsesSyncedOpenClawEndpoint(t *testing.T) {
func TestResolveGatewayReportedRemoteAddressUsesBuiltInOpenClawEndpoint(t *testing.T) {
t.Parallel()
server := NewServer()
server.syncProviders([]syncedProvider{
{
ProviderID: "openclaw",
Label: "OpenClaw",
Endpoint: "wss://gateway.example.com",
Enabled: true,
},
})
got := resolveGatewayReportedRemoteAddress(server, gatewayruntime.ConnectRequest{
Mode: "remote",
@ -28,13 +20,15 @@ func TestResolveGatewayReportedRemoteAddressUsesSyncedOpenClawEndpoint(t *testin
},
})
const want = "gateway.example.com:443"
const want = "openclaw.svc.plus:443"
if got != want {
t.Fatalf("resolveGatewayReportedRemoteAddress() = %q, want %q", got, want)
}
}
func TestResolveGatewayReportedRemoteAddressPreservesExplicitPublicRemoteHost(t *testing.T) {
func TestResolveGatewayReportedRemoteAddressNormalizesExplicitPublicRemoteHost(
t *testing.T,
) {
t.Parallel()
server := NewServer()
@ -48,7 +42,8 @@ func TestResolveGatewayReportedRemoteAddressPreservesExplicitPublicRemoteHost(t
},
})
if got != "" {
t.Fatalf("expected explicit public remote host to bypass override, got %q", got)
const want = "openclaw.svc.plus:443"
if got != want {
t.Fatalf("resolveGatewayReportedRemoteAddress() = %q, want %q", got, want)
}
}

View File

@ -2,8 +2,19 @@ package acp
import (
"strings"
"xworkmate-bridge/internal/shared"
)
const (
productionGatewayEndpointURL = "wss://openclaw.svc.plus"
productionCodexEndpointURL = "https://acp-server.svc.plus/codex/acp/rpc"
productionOpenCodeEndpointURL = "https://acp-server.svc.plus/opencode/acp/rpc"
productionGeminiEndpointURL = "https://acp-server.svc.plus/gemini/acp/rpc"
)
var productionProviderOrder = []string{"codex", "opencode", "gemini"}
type syncedProvider struct {
ProviderID string
Label string
@ -12,47 +23,40 @@ type syncedProvider struct {
Enabled bool
}
func parseSyncedProviders(raw any) []syncedProvider {
list, ok := raw.([]any)
if !ok {
return nil
func newProductionProviderCatalog() (map[string]syncedProvider, []string) {
authorizationHeader := normalizeAuthorizationHeader(
strings.TrimSpace(shared.EnvOrDefault("INTERNAL_SERVICE_TOKEN", "")),
)
providers := []syncedProvider{
{
ProviderID: "codex",
Label: "Codex",
Endpoint: productionCodexEndpointURL,
AuthorizationHeader: authorizationHeader,
Enabled: true,
},
{
ProviderID: "opencode",
Label: "OpenCode",
Endpoint: productionOpenCodeEndpointURL,
AuthorizationHeader: authorizationHeader,
Enabled: true,
},
{
ProviderID: "gemini",
Label: "Gemini",
Endpoint: productionGeminiEndpointURL,
AuthorizationHeader: authorizationHeader,
Enabled: true,
},
}
providers := make([]syncedProvider, 0, len(list))
for _, item := range list {
entry := asMap(item)
providerID := strings.TrimSpace(sharedString(entry, "providerId"))
if providerID == "" {
continue
}
providers = append(providers, syncedProvider{
ProviderID: providerID,
Label: strings.TrimSpace(sharedString(entry, "label")),
Endpoint: strings.TrimSpace(sharedString(entry, "endpoint")),
AuthorizationHeader: strings.TrimSpace(sharedString(entry, "authorizationHeader")),
Enabled: parseBool(entry["enabled"]),
})
}
return providers
}
func (s *Server) syncProviders(providers []syncedProvider) map[string]any {
s.mu.Lock()
defer s.mu.Unlock()
s.providerCatalog = make(map[string]syncedProvider, len(providers))
s.providerOrder = make([]string, 0, len(providers))
catalog := make(map[string]syncedProvider, len(providers))
order := make([]string, 0, len(providers))
for _, provider := range providers {
providerID := strings.TrimSpace(provider.ProviderID)
if providerID == "" {
continue
}
provider.ProviderID = providerID
s.providerCatalog[providerID] = provider
s.providerOrder = append(s.providerOrder, providerID)
}
return map[string]any{
"ok": true,
"providers": syncedProvidersResult(providers),
catalog[provider.ProviderID] = provider
order = append(order, provider.ProviderID)
}
return catalog, order
}
func (s *Server) syncedProviderByID(providerID string) (syncedProvider, bool) {
@ -102,19 +106,6 @@ func (s *Server) availableProviderCatalog() []map[string]any {
return result
}
func syncedProvidersResult(providers []syncedProvider) []map[string]any {
result := make([]map[string]any, 0, len(providers))
for _, provider := range providers {
result = append(result, map[string]any{
"providerId": provider.ProviderID,
"label": providerLabel(provider),
"endpoint": provider.Endpoint,
"enabled": provider.Enabled,
})
}
return result
}
func providerLabel(provider syncedProvider) string {
if label := strings.TrimSpace(provider.Label); label != "" {
return label

View File

@ -13,81 +13,20 @@ import (
"xworkmate-bridge/internal/shared"
)
func TestProvidersSyncUpdatesCapabilities(t *testing.T) {
server := NewServer()
_, rpcErr := server.handleRequest(shared.RPCRequest{
Method: "xworkmate.providers.sync",
Params: map[string]any{
"providers": []any{
map[string]any{
"providerId": "claude",
"label": "Claude",
"endpoint": "http://127.0.0.1:9999",
"authorizationHeader": "Bearer test",
"enabled": true,
},
},
},
}, func(map[string]any) {})
if rpcErr != nil {
t.Fatalf("expected sync success, got %v", rpcErr)
}
result, rpcErr := server.handleRequest(shared.RPCRequest{
Method: "acp.capabilities",
Params: map[string]any{},
}, func(map[string]any) {})
if rpcErr != nil {
t.Fatalf("expected capabilities success, got %v", rpcErr)
}
providerCatalog, ok := result["providerCatalog"].([]map[string]any)
if !ok || len(providerCatalog) == 0 {
t.Fatalf("expected synced provider in capabilities, got %#v", result)
}
if providerCatalog[0]["providerId"] != "claude" {
t.Fatalf("expected claude provider after sync, got %#v", providerCatalog)
}
if providerCatalog[0]["label"] != "Claude" {
t.Fatalf("expected Claude label after sync, got %#v", providerCatalog)
func setTestBridgeProvider(server *Server, provider syncedProvider) {
server.mu.Lock()
defer server.mu.Unlock()
if server.providerCatalog == nil {
server.providerCatalog = map[string]syncedProvider{}
}
providerID := strings.TrimSpace(provider.ProviderID)
provider.ProviderID = providerID
server.providerCatalog[providerID] = provider
}
func TestProvidersSyncPreservesProviderCatalogOrder(t *testing.T) {
func TestCapabilitiesExposeBuiltInProductionProviderCatalog(t *testing.T) {
server := NewServer()
_, rpcErr := server.handleRequest(shared.RPCRequest{
Method: "xworkmate.providers.sync",
Params: map[string]any{
"providers": []any{
map[string]any{
"providerId": "gemini",
"label": "Gemini",
"endpoint": "http://127.0.0.1:9001",
"authorizationHeader": "Bearer gemini",
"enabled": true,
},
map[string]any{
"providerId": "codex",
"label": "Codex",
"endpoint": "http://127.0.0.1:9002",
"authorizationHeader": "Bearer codex",
"enabled": true,
},
map[string]any{
"providerId": "opencode",
"label": "OpenCode",
"endpoint": "http://127.0.0.1:9003",
"authorizationHeader": "Bearer opencode",
"enabled": true,
},
},
},
}, func(map[string]any) {})
if rpcErr != nil {
t.Fatalf("expected sync success, got %v", rpcErr)
}
result, rpcErr := server.handleRequest(shared.RPCRequest{
Method: "acp.capabilities",
Params: map[string]any{},
@ -100,22 +39,37 @@ func TestProvidersSyncPreservesProviderCatalogOrder(t *testing.T) {
t.Fatalf("expected providerCatalog array, got %#v", result)
}
if len(providerCatalog) != 3 {
t.Fatalf("expected 3 catalog entries, got %#v", providerCatalog)
t.Fatalf("expected 3 built-in providers, got %#v", providerCatalog)
}
gotOrder := []string{
providerCatalog[0]["providerId"].(string),
providerCatalog[1]["providerId"].(string),
providerCatalog[2]["providerId"].(string),
}
wantOrder := []string{"gemini", "codex", "opencode"}
wantOrder := []string{"codex", "opencode", "gemini"}
wantLabels := []string{"Codex", "OpenCode", "Gemini"}
for index, want := range wantOrder {
if gotOrder[index] != want {
t.Fatalf("expected provider order %#v, got %#v", wantOrder, gotOrder)
if got := providerCatalog[index]["providerId"]; got != want {
t.Fatalf("expected provider %q at index %d, got %#v", want, index, providerCatalog)
}
if got := providerCatalog[index]["label"]; got != wantLabels[index] {
t.Fatalf("expected label %q at index %d, got %#v", wantLabels[index], index, providerCatalog)
}
}
}
func TestExecuteSessionTaskUsesSyncedExternalProvider(t *testing.T) {
func TestProvidersSyncMethodIsRemovedFromProductionFlow(t *testing.T) {
server := NewServer()
_, rpcErr := server.handleRequest(shared.RPCRequest{
Method: "xworkmate.providers.sync",
}, func(map[string]any) {})
if rpcErr == nil {
t.Fatalf("expected xworkmate.providers.sync to be unavailable")
}
if rpcErr.Code != -32601 {
t.Fatalf("expected unknown method error, got %#v", rpcErr)
}
if !strings.Contains(rpcErr.Message, "xworkmate.providers.sync") {
t.Fatalf("expected method name in error, got %#v", rpcErr)
}
}
func TestExecuteSessionTaskUsesBuiltInProductionProvider(t *testing.T) {
var lastForwardedParams map[string]any
externalServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/acp/rpc" {
@ -140,7 +94,7 @@ func TestExecuteSessionTaskUsesSyncedExternalProvider(t *testing.T) {
"success": true,
"output": "external-provider-ok",
"turnId": "turn-external",
"provider": "claude",
"provider": "codex",
"mode": "single-agent",
},
})
@ -155,14 +109,13 @@ func TestExecuteSessionTaskUsesSyncedExternalProvider(t *testing.T) {
defer externalServer.Close()
server := NewServer()
server.syncProviders([]syncedProvider{
{
ProviderID: "claude",
Label: "Claude",
Endpoint: externalServer.URL,
AuthorizationHeader: "Bearer test",
Enabled: true,
},
t.Setenv("INTERNAL_SERVICE_TOKEN", "internal-test-token")
setTestBridgeProvider(server, syncedProvider{
ProviderID: "codex",
Label: "Codex",
Endpoint: externalServer.URL,
AuthorizationHeader: "Bearer internal-test-token",
Enabled: true,
})
response, rpcErr := server.executeSessionTask(task{
@ -176,7 +129,7 @@ func TestExecuteSessionTaskUsesSyncedExternalProvider(t *testing.T) {
"routing": map[string]any{
"routingMode": "explicit",
"explicitExecutionTarget": "singleAgent",
"explicitProviderId": "claude",
"explicitProviderId": "codex",
},
},
},
@ -187,15 +140,12 @@ func TestExecuteSessionTaskUsesSyncedExternalProvider(t *testing.T) {
if got := response["output"]; got != "external-provider-ok" {
t.Fatalf("expected external provider output, got %#v", response)
}
if got := response["resolvedProviderId"]; got != "claude" {
t.Fatalf("expected resolved provider claude, got %#v", response)
if got := response["resolvedProviderId"]; got != "codex" {
t.Fatalf("expected resolved provider codex, got %#v", response)
}
if _, exists := lastForwardedParams["metadata"]; exists {
t.Fatalf("expected metadata to be stripped for external provider request, got %#v", lastForwardedParams)
}
if _, exists := lastForwardedParams[externalProviderEndpointKey]; exists {
t.Fatalf("expected internal endpoint key to be stripped, got %#v", lastForwardedParams)
}
}
func TestExecuteSessionTaskEnrichesExternalProviderResultWithArtifactsAndRemoteMetadata(t *testing.T) {
@ -240,14 +190,12 @@ func TestExecuteSessionTaskEnrichesExternalProviderResultWithArtifactsAndRemoteM
defer externalServer.Close()
server := NewServer()
server.syncProviders([]syncedProvider{
{
ProviderID: "claude",
Label: "Claude",
Endpoint: externalServer.URL,
AuthorizationHeader: "Bearer test",
Enabled: true,
},
setTestBridgeProvider(server, syncedProvider{
ProviderID: "codex",
Label: "Codex",
Endpoint: externalServer.URL,
AuthorizationHeader: "Bearer internal-test-token",
Enabled: true,
})
response, rpcErr := server.executeSessionTask(task{
@ -261,7 +209,7 @@ func TestExecuteSessionTaskEnrichesExternalProviderResultWithArtifactsAndRemoteM
"routing": map[string]any{
"routingMode": "explicit",
"explicitExecutionTarget": "singleAgent",
"explicitProviderId": "claude",
"explicitProviderId": "codex",
},
},
},
@ -298,58 +246,6 @@ func TestExecuteSessionTaskEnrichesExternalProviderResultWithArtifactsAndRemoteM
}
}
func TestRunSingleAgentUsesFrozenExternalProviderParams(t *testing.T) {
externalServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/acp/rpc" {
http.NotFound(w, r)
return
}
defer func() {
_ = r.Body.Close()
}()
var request map[string]any
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Fatalf("decode request: %v", err)
}
_ = json.NewEncoder(w).Encode(map[string]any{
"jsonrpc": "2.0",
"id": request["id"],
"result": map[string]any{
"success": true,
"output": "frozen-provider-ok",
"turnId": "turn-frozen",
"provider": "custom-agent-1",
"mode": "single-agent",
},
})
}))
defer externalServer.Close()
server := NewServer()
session := server.getOrCreateSession("session-frozen", "thread-frozen")
result := server.runSingleAgent(
context.Background(),
"session.start",
session,
map[string]any{
"provider": "custom-agent-1",
"taskPrompt": "hello",
"workingDirectory": t.TempDir(),
externalProviderEndpointKey: externalServer.URL,
externalProviderAuthorizationHeaderKey: "Bearer test",
externalProviderLabelKey: "Codex",
},
"turn-frozen",
func(map[string]any) {},
)
if result.err != nil {
t.Fatalf("expected success, got rpc error: %v", result.err)
}
if got := result.response["output"]; got != "frozen-provider-ok" {
t.Fatalf("expected frozen provider output, got %#v", result.response)
}
}
func TestRunSingleAgentRequiresAdvertisedProvider(t *testing.T) {
server := NewServer()
session := server.getOrCreateSession("session-local", "thread-local")
@ -358,7 +254,7 @@ func TestRunSingleAgentRequiresAdvertisedProvider(t *testing.T) {
"session.start",
session,
map[string]any{
"provider": "opencode",
"provider": "claude",
"taskPrompt": "hello",
"workingDirectory": filepath.Join(t.TempDir(), "missing"),
},
@ -392,14 +288,15 @@ func TestHandleRPCRequiresExplicitBearerForExternalProvider(t *testing.T) {
}))
defer externalServer.Close()
t.Setenv("INTERNAL_SERVICE_TOKEN", "synced-provider-token")
server := NewServer()
server.syncProviders([]syncedProvider{{
setTestBridgeProvider(server, syncedProvider{
ProviderID: "codex",
Label: "Codex",
Endpoint: externalServer.URL,
AuthorizationHeader: "Bearer synced-provider-token",
Enabled: true,
}})
})
recorder := httptest.NewRecorder()
request := httptest.NewRequest(

View File

@ -175,20 +175,20 @@ func TestExecuteSessionTaskAutoRoutingRecordsProjectMemory(t *testing.T) {
t.Setenv("HOME", homeDir)
server := NewServer()
providerServer := newExternalSingleAgentProvider(t, "claude", "done")
providerServer := newExternalSingleAgentProvider(t, "codex", "done")
defer providerServer.Close()
server.syncProviders([]syncedProvider{{
ProviderID: "claude",
Label: "Claude",
setTestBridgeProvider(server, syncedProvider{
ProviderID: "codex",
Label: "Codex",
Endpoint: providerServer.URL,
Enabled: true,
}})
})
response, rpcErr := server.executeSessionTask(task{
req: shared.RPCRequest{
Params: map[string]any{
"sessionId": "session-auto",
"threadId": "thread-auto",
"provider": "claude",
"provider": "codex",
"taskPrompt": "create a powerpoint deck for launch",
"workingDirectory": workspaceDir,
"routing": map[string]any{
@ -246,26 +246,26 @@ func TestExecuteSessionTaskExplicitRoutingDoesNotRecordProjectMemory(t *testing.
t.Setenv("HOME", homeDir)
server := NewServer()
providerServer := newExternalSingleAgentProvider(t, "claude", "done")
providerServer := newExternalSingleAgentProvider(t, "codex", "done")
defer providerServer.Close()
server.syncProviders([]syncedProvider{{
ProviderID: "claude",
Label: "Claude",
setTestBridgeProvider(server, syncedProvider{
ProviderID: "codex",
Label: "Codex",
Endpoint: providerServer.URL,
Enabled: true,
}})
})
response, rpcErr := server.executeSessionTask(task{
req: shared.RPCRequest{
Params: map[string]any{
"sessionId": "session-explicit",
"threadId": "thread-explicit",
"provider": "claude",
"provider": "codex",
"taskPrompt": "create a powerpoint deck for launch",
"workingDirectory": workspaceDir,
"routing": map[string]any{
"routingMode": "explicit",
"explicitExecutionTarget": "singleAgent",
"explicitProviderId": "claude",
"explicitProviderId": "codex",
"availableSkills": []any{
map[string]any{
"id": "pptx",
@ -330,7 +330,7 @@ func TestExecuteSessionTaskExplicitProviderRequiresAdvertisedBridgeProvider(t *t
}
}
func TestExecuteSessionTaskAutoRoutingUsesBridgeSyncOrderForProviderResolution(t *testing.T) {
func TestExecuteSessionTaskAutoRoutingUsesBridgeProductionProviderOrder(t *testing.T) {
workspaceDir := filepath.Join(t.TempDir(), "workspace")
if err := os.MkdirAll(workspaceDir, 0o755); err != nil {
t.Fatalf("create workspace: %v", err)
@ -341,19 +341,17 @@ func TestExecuteSessionTaskAutoRoutingUsesBridgeSyncOrderForProviderResolution(t
defer geminiProvider.Close()
codexProvider := newExternalSingleAgentProvider(t, "codex", "codex-output")
defer codexProvider.Close()
server.syncProviders([]syncedProvider{
{
ProviderID: "gemini",
Label: "Gemini",
Endpoint: geminiProvider.URL,
Enabled: true,
},
{
ProviderID: "codex",
Label: "Codex",
Endpoint: codexProvider.URL,
Enabled: true,
},
setTestBridgeProvider(server, syncedProvider{
ProviderID: "gemini",
Label: "Gemini",
Endpoint: geminiProvider.URL,
Enabled: true,
})
setTestBridgeProvider(server, syncedProvider{
ProviderID: "codex",
Label: "Codex",
Endpoint: codexProvider.URL,
Enabled: true,
})
response, rpcErr := server.executeSessionTask(task{
@ -382,8 +380,8 @@ func TestExecuteSessionTaskAutoRoutingUsesBridgeSyncOrderForProviderResolution(t
if rpcErr != nil {
t.Fatalf("expected success, got rpc error: %v", rpcErr)
}
if got := response["resolvedProviderId"]; got != "gemini" {
t.Fatalf("expected resolved provider gemini from bridge order, got %#v", response)
if got := response["resolvedProviderId"]; got != "codex" {
t.Fatalf("expected resolved provider codex from built-in bridge order, got %#v", response)
}
}

View File

@ -104,12 +104,13 @@ func Serve(args []string) error {
}
func NewServer() *Server {
providerCatalog, providerOrder := newProductionProviderCatalog()
return &Server{
sessions: make(map[string]*session),
queues: make(map[string]chan task),
gateway: gatewayruntime.NewManager(),
providerCatalog: make(map[string]syncedProvider),
providerOrder: nil,
providerCatalog: providerCatalog,
providerOrder: providerOrder,
authService: service.NewStaticTokenAuthService(strings.TrimSpace(shared.EnvOrDefault("ACP_AUTH_TOKEN", ""))),
}
}
@ -372,8 +373,6 @@ func (s *Server) handleRequest(
s.availableProviders(),
)
return mergeRoutingResponse(map[string]any{"ok": true}, result), nil
case "xworkmate.providers.sync":
return s.syncProviders(parseSyncedProviders(request.Params["providers"])), nil
case "xworkmate.mounts.reconcile":
return handleMountReconcile(request.Params), nil
case "xworkmate.gateway.connect":
@ -616,14 +615,6 @@ func (s *Server) executeSessionTask(task task) (map[string]any, *shared.RPCError
executionParams := buildResolvedExecutionParams(params, resolvedRouting)
mode := strings.TrimSpace(shared.StringArg(executionParams, "mode", "single-agent"))
provider := strings.TrimSpace(shared.StringArg(executionParams, "provider", ""))
if provider != "" {
if syncedProvider, ok := s.syncedProviderByID(provider); ok {
executionParams = injectResolvedExternalProviderParams(
executionParams,
syncedProvider,
)
}
}
session := s.getOrCreateSession(sessionID, threadID)
session.mode = mode
@ -716,51 +707,6 @@ func (s *Server) runSingleAgent(
workingDirectory,
)
if syncedProvider, ok := externalProviderFromParams(params); ok {
response, err := s.runSingleAgentViaExternalProvider(
ctx,
syncedProvider,
method,
params,
notify,
)
if err == nil {
result := asMap(response["result"])
if len(result) == 0 {
result = response
}
if _, exists := result["provider"]; !exists {
result["provider"] = provider
}
if _, exists := result["mode"]; !exists {
result["mode"] = "single-agent"
}
if _, exists := result["turnId"]; !exists {
result["turnId"] = turnID
}
if _, exists := result["effectiveWorkingDirectory"]; !exists && effectiveWorkingDirectory != "" {
result["effectiveWorkingDirectory"] = effectiveWorkingDirectory
}
return taskResult{response: enrichSingleAgentResultArtifacts(result, params)}
}
s.emitSessionUpdate(session, notify, turnID, map[string]any{
"type": "status",
"event": "completed",
"message": err.Error(),
"pending": false,
"error": true,
})
return taskResult{
response: map[string]any{
"success": false,
"error": err.Error(),
"turnId": turnID,
"mode": "single-agent",
"provider": provider,
},
}
}
if syncedProvider, ok := s.syncedProviderByID(provider); ok {
response, err := s.runSingleAgentViaExternalProvider(
ctx,
@ -795,13 +741,6 @@ func (s *Server) runSingleAgent(
"pending": false,
"error": true,
})
s.emitSessionUpdate(session, notify, turnID, map[string]any{
"type": "status",
"event": "completed",
"message": err.Error(),
"pending": false,
"error": true,
})
return taskResult{
response: map[string]any{
"success": false,