Make production provider routing bridge-owned
This commit is contained in:
parent
ca66dee7b3
commit
d1bec8d253
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -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。
|
||||
|
||||
### 执行层断言
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -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(
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user