diff --git a/docs/api-reference.md b/docs/api-reference.md index b07c9b7..e86f5e9 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -134,7 +134,6 @@ Response shape: { "providerId": "gemini", "label": "Gemini" } ], "gatewayProviders": [ - { "providerId": "local", "label": "Local" }, { "providerId": "openclaw", "label": "OpenClaw" } ], "capabilities": { @@ -146,7 +145,6 @@ Response shape: { "providerId": "gemini", "label": "Gemini" } ], "gatewayProviders": [ - { "providerId": "local", "label": "Local" }, { "providerId": "openclaw", "label": "OpenClaw" } ] } @@ -164,7 +162,8 @@ Notes: - `gemini` -> `https://acp-server.svc.plus/gemini/acp/rpc` - APP traffic reaches those upstreams through the bridge's canonical public ACP path, not by depending on upstream URLs directly -- upstream ACP auth uses `Authorization: Bearer $INTERNAL_SERVICE_TOKEN` +- upstream ACP auth prefers bridge-owned service auth and otherwise reuses the + inbound bridge bearer header - `multiAgent` is controlled by `ACP_MULTI_AGENT_ENABLED`, default `true` ### 3.2 `session.start` @@ -405,7 +404,7 @@ Suggested APP-side view model: { "executionTargets": ["single-agent", "multi-agent", "gateway"], "singleAgentProviders": ["codex", "opencode", "gemini"], - "gatewayProviders": ["local", "openclaw"] + "gatewayProviders": ["openclaw"] } ``` @@ -426,7 +425,8 @@ UI binding guidance: - provider picker for single-agent mode should be populated from `providerCatalog` - gateway picker should be populated from `gatewayProviders` -- gateway UI should display `local` and `openclaw` as selectable providers +- gateway UI should display the bridge-owned `openclaw` provider from + `gatewayProviders` - disabled or unavailable states should come from `xworkmate.routing.resolve` response fields such as: - `unavailable` @@ -509,10 +509,10 @@ Response fields: Notes: +- empty or unsupported `gatewayProviderId` values are normalized to + `openclaw` - for `gatewayProviderId=openclaw`, the bridge overrides runtime endpoint selection to `wss://openclaw.svc.plus` -- for `gatewayProviderId=local`, the bridge keeps the caller-provided local - gateway endpoint configuration - upstream gateway auth uses `Authorization: Bearer $INTERNAL_SERVICE_TOKEN` - the app does not provide production openclaw endpoint truth diff --git a/docs/architecture/adr-unified-bridge-entrypoints.md b/docs/architecture/adr-unified-bridge-entrypoints.md index 11f2a1d..e9e27e1 100644 --- a/docs/architecture/adr-unified-bridge-entrypoints.md +++ b/docs/architecture/adr-unified-bridge-entrypoints.md @@ -65,7 +65,7 @@ If the bridge reports execution-target metadata such as `single-agent`, `multi-agent`, or `gateway`, the app should treat those values as routing results, not as shell-level surface categories. -If the bridge reports gateway provider IDs such as `local` or `openclaw`, the +If the bridge reports gateway provider IDs such as `openclaw`, the app should treat them as bridge-owned gateway backend identifiers, not as independent app entrypoints. diff --git a/docs/xworkmate-bridge-svc-plus-core-functional-test-plan-v1.md b/docs/xworkmate-bridge-svc-plus-core-functional-test-plan-v1.md index 50f6b4e..40fbb81 100644 --- a/docs/xworkmate-bridge-svc-plus-core-functional-test-plan-v1.md +++ b/docs/xworkmate-bridge-svc-plus-core-functional-test-plan-v1.md @@ -16,7 +16,7 @@ - `acp.capabilities` 返回动态 provider 列表。 - 至少覆盖: - `singleAgentProviders`: `opencode / codex / gemini` - - `gatewayProviders`: `local / openclaw` + - `gatewayProviders`: `openclaw` - `xworkmate.routing.resolve` 根据 `taskPrompt`、`executionTarget`、`selectedSkills` 返回正确的: - `resolvedExecutionTarget` - `resolvedProviderId` @@ -101,7 +101,7 @@ flutter test test/runtime/app_controller_single_agent_workspace_binding_regressi - `acp.capabilities` 的 provider 列表来自 bridge 当前环境,而不是本地写死。 - bridge 内建生产 catalog 包含 `codex / opencode / gemini`,且不依赖 app 侧预同步。 -- bridge 还会暴露 `gatewayProviders = local / openclaw`。 +- bridge 还会暴露 `gatewayProviders = openclaw`。 - `xworkmate.routing.resolve` 在 skill / prompt / target 组合下,返回合理的 `resolvedProviderId` 或 `resolvedGatewayProviderId`。 diff --git a/internal/acp/execution.go b/internal/acp/execution.go index 188b66d..cba7cec 100644 --- a/internal/acp/execution.go +++ b/internal/acp/execution.go @@ -82,7 +82,7 @@ func (s *Server) runGateway( _ = ctx gatewayProvider := strings.TrimSpace(shared.StringArg(params, "gatewayProvider", "")) if gatewayProvider == "" { - gatewayProvider = router.GatewayProviderLocal + gatewayProvider = router.GatewayProviderOpenClaw } result := s.gateway.RequestByMode( gatewayProvider, @@ -145,10 +145,14 @@ func (s *Server) runSingleAgentViaExternalProvider( notify(message) } } + authorization := firstNonEmptyString( + strings.TrimSpace(provider.AuthorizationHeader), + strings.TrimSpace(shared.StringArg(params, inboundAuthorizationHeaderKey, "")), + ) response, err := requestExternalACP( ctx, endpoint, - provider.AuthorizationHeader, + authorization, method, forwardParams, combinedNotify, diff --git a/internal/acp/gateway_runtime.go b/internal/acp/gateway_runtime.go index 25d3d0b..45acc9c 100644 --- a/internal/acp/gateway_runtime.go +++ b/internal/acp/gateway_runtime.go @@ -54,7 +54,7 @@ func handleGatewayConnect( }, } if request.Mode == "" { - request.Mode = "local" + request.Mode = "openclaw" } request = applyProductionGatewayRouting(request) request.ReportedRemoteAddress = resolveGatewayReportedRemoteAddress(server, request) diff --git a/internal/acp/provider_catalog.go b/internal/acp/provider_catalog.go index f475343..8667e63 100644 --- a/internal/acp/provider_catalog.go +++ b/internal/acp/provider_catalog.go @@ -121,10 +121,6 @@ func providerLabel(provider syncedProvider) string { func availableGatewayProviderCatalog() []map[string]any { return []map[string]any{ - { - "providerId": router.GatewayProviderLocal, - "label": "Local", - }, { "providerId": router.GatewayProviderOpenClaw, "label": "OpenClaw", diff --git a/internal/acp/providers_sync_test.go b/internal/acp/providers_sync_test.go index 2b9bc59..8404c8e 100644 --- a/internal/acp/providers_sync_test.go +++ b/internal/acp/providers_sync_test.go @@ -45,8 +45,8 @@ func TestCapabilitiesExposeBuiltInProductionProviderCatalog(t *testing.T) { if len(providerCatalog) != 3 { t.Fatalf("expected 3 built-in providers, got %#v", providerCatalog) } - if len(gatewayProviders) != 2 { - t.Fatalf("expected 2 built-in gateway providers, got %#v", gatewayProviders) + if len(gatewayProviders) != 1 { + t.Fatalf("expected 1 built-in gateway provider, got %#v", gatewayProviders) } wantOrder := []string{"codex", "opencode", "gemini"} wantLabels := []string{"Codex", "OpenCode", "Gemini"} @@ -58,8 +58,8 @@ func TestCapabilitiesExposeBuiltInProductionProviderCatalog(t *testing.T) { t.Fatalf("expected label %q at index %d, got %#v", wantLabels[index], index, providerCatalog) } } - wantGatewayOrder := []string{"local", "openclaw"} - wantGatewayLabels := []string{"Local", "OpenClaw"} + wantGatewayOrder := []string{"openclaw"} + wantGatewayLabels := []string{"OpenClaw"} for index, want := range wantGatewayOrder { if got := gatewayProviders[index]["providerId"]; got != want { t.Fatalf("expected gateway provider %q at index %d, got %#v", want, index, gatewayProviders) @@ -70,6 +70,51 @@ func TestCapabilitiesExposeBuiltInProductionProviderCatalog(t *testing.T) { } } +func TestBuiltInProviderReusesInboundBridgeBearerWhenUpstreamAuthUnset(t *testing.T) { + externalServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got := r.Header.Get("Authorization"); got != "Bearer bridge-token" { + t.Fatalf("expected inbound bridge bearer header, got %q", got) + } + _ = json.NewEncoder(w).Encode(map[string]any{ + "jsonrpc": "2.0", + "id": "run-auth-fallback", + "result": map[string]any{ + "success": true, + "output": "forwarded-auth-fallback-ok", + }, + }) + })) + defer externalServer.Close() + + t.Setenv("INTERNAL_SERVICE_TOKEN", "") + t.Setenv("BRIDGE_AUTH_TOKEN", "") + server := NewServer() + setTestBridgeProvider(server, syncedProvider{ + ProviderID: "codex", + Label: "Codex", + Endpoint: externalServer.URL, + Enabled: true, + }) + + recorder := httptest.NewRecorder() + request := httptest.NewRequest( + http.MethodPost, + "http://127.0.0.1/acp/rpc", + strings.NewReader(`{"jsonrpc":"2.0","id":"run-auth-fallback","method":"session.start","params":{"sessionId":"s1","threadId":"t1","taskPrompt":"hello","workingDirectory":"`+t.TempDir()+`","routing":{"routingMode":"explicit","explicitExecutionTarget":"singleAgent","explicitProviderId":"codex"}}}`), + ) + request.Header.Set("Content-Type", "application/json") + request.Header.Set("Authorization", "Bearer bridge-token") + + server.HandleRPC(recorder, request) + + if recorder.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", recorder.Code) + } + if !strings.Contains(recorder.Body.String(), "forwarded-auth-fallback-ok") { + t.Fatalf("expected forwarded provider response, got %q", recorder.Body.String()) + } +} + func TestProductionProviderCatalogFallsBackToBridgeAuthToken(t *testing.T) { t.Setenv("INTERNAL_SERVICE_TOKEN", "") t.Setenv("BRIDGE_AUTH_TOKEN", "bridge-auth-token") diff --git a/internal/acp/routing_test.go b/internal/acp/routing_test.go index 4b0d606..ad7f1f6 100644 --- a/internal/acp/routing_test.go +++ b/internal/acp/routing_test.go @@ -102,7 +102,7 @@ func TestHandleRoutingResolveCoversNineScenarioBuckets(t *testing.T) { name: "image-cog", prompt: "use image-cog to generate consistent characters", expectedExecutionTarget: "gateway", - expectedGatewayProviderID: "local", + expectedGatewayProviderID: "openclaw", expectedSkillSource: "find_skills", expectedNeedsSkillInstall: true, }, @@ -110,7 +110,7 @@ func TestHandleRoutingResolveCoversNineScenarioBuckets(t *testing.T) { name: "image-video-generation-editting", prompt: "wan 图生视频并做视频编辑", expectedExecutionTarget: "gateway", - expectedGatewayProviderID: "local", + expectedGatewayProviderID: "openclaw", expectedSkillSource: "find_skills", expectedNeedsSkillInstall: true, }, @@ -118,7 +118,7 @@ func TestHandleRoutingResolveCoversNineScenarioBuckets(t *testing.T) { name: "video-translator", prompt: "translate video subtitles and dub the clip", expectedExecutionTarget: "gateway", - expectedGatewayProviderID: "local", + expectedGatewayProviderID: "openclaw", expectedSkillSource: "find_skills", expectedNeedsSkillInstall: true, }, @@ -126,7 +126,7 @@ func TestHandleRoutingResolveCoversNineScenarioBuckets(t *testing.T) { name: "browser-search-news", prompt: "跨浏览器执行并搜索最新资讯采集结果", expectedExecutionTarget: "gateway", - expectedGatewayProviderID: "local", + expectedGatewayProviderID: "openclaw", expectedSkillSource: "local_match", expectedResolvedSkill: "Browser Automation", }, @@ -139,7 +139,7 @@ func TestHandleRoutingResolveCoversNineScenarioBuckets(t *testing.T) { "workingDirectory": "/tmp/workspace", "routing": map[string]any{ "routingMode": "auto", - "preferredGatewayProviderId": "local", + "preferredGatewayProviderId": "openclaw", "allowSkillInstall": false, "availableSkills": func() []any { values := make([]any, 0, len(localAvailableSkills)) diff --git a/internal/router/router.go b/internal/router/router.go index 6aa02d4..c08cf4d 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -17,7 +17,6 @@ const ( ExecutionTargetGateway = "gateway" ExecutionTargetGatewayChat = "gateway-chat" - GatewayProviderLocal = "local" GatewayProviderOpenClaw = "openclaw" ) @@ -223,15 +222,13 @@ func mapExplicitTarget( func resolveGatewayProvider(preferredGatewayProviderID string) string { providerID := normalizeGatewayProvider(preferredGatewayProviderID) if providerID == "" { - providerID = GatewayProviderLocal + providerID = GatewayProviderOpenClaw } return providerID } func normalizeGatewayProvider(value string) string { switch normalize(value) { - case GatewayProviderLocal: - return GatewayProviderLocal case GatewayProviderOpenClaw: return GatewayProviderOpenClaw default: diff --git a/internal/router/router_test.go b/internal/router/router_test.go index ac3b2da..112a831 100644 --- a/internal/router/router_test.go +++ b/internal/router/router_test.go @@ -84,14 +84,14 @@ func TestResolveAutoOnlineTaskToGateway(t *testing.T) { result := resolver.Resolve(Request{ Prompt: "跨浏览器执行并搜索最新资讯", - PreferredGatewayProviderID: GatewayProviderLocal, + PreferredGatewayProviderID: GatewayProviderOpenClaw, }) if result.ResolvedExecutionTarget != ExecutionTargetGateway { t.Fatalf("expected gateway route, got %#v", result) } - if result.ResolvedGatewayProviderID != GatewayProviderLocal { - t.Fatalf("expected local gateway provider, got %#v", result) + if result.ResolvedGatewayProviderID != GatewayProviderOpenClaw { + t.Fatalf("expected openclaw gateway provider, got %#v", result) } } @@ -121,14 +121,14 @@ func TestResolveUsesClassifierForBoundarySamples(t *testing.T) { result := resolver.Resolve(Request{ Prompt: "help me handle this ambiguous request", - PreferredGatewayProviderID: GatewayProviderLocal, + PreferredGatewayProviderID: GatewayProviderOpenClaw, }) if result.ResolvedExecutionTarget != ExecutionTargetGateway { t.Fatalf("expected classifier to resolve gateway route, got %#v", result) } - if result.ResolvedGatewayProviderID != GatewayProviderLocal { - t.Fatalf("expected local gateway provider, got %#v", result) + if result.ResolvedGatewayProviderID != GatewayProviderOpenClaw { + t.Fatalf("expected openclaw gateway provider, got %#v", result) } }