From 01978946980290d9dd5606bb84606389a59de892 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Thu, 7 May 2026 08:44:23 +0800 Subject: [PATCH] fix: use openclaw task submit endpoint --- .../bridge-runtime-routing-map.md | 12 +- .../unified-routing-architecture.md | 14 +- .../app-external-service-api-test-matrix.md | 15 +- ...pp_controller_desktop_runtime_helpers.dart | 11 + lib/runtime/gateway_acp_client.dart | 36 +- .../runtime/gateway_acp_client_auth_test.dart | 320 ++++++++++-------- 6 files changed, 250 insertions(+), 158 deletions(-) diff --git a/docs/architecture/bridge-runtime-routing-map.md b/docs/architecture/bridge-runtime-routing-map.md index db70e956..64aa045a 100644 --- a/docs/architecture/bridge-runtime-routing-map.md +++ b/docs/architecture/bridge-runtime-routing-map.md @@ -4,7 +4,7 @@ Last Updated: 2026-04-21 本文记录 `xworkmate-app` 当前对 `xworkmate-bridge` 的运行时路由合同。UI 不直接承载这些路径;Assistant UI 仍由 `acp.capabilities` 返回的 `providerCatalog`、`gatewayProviders`、`availableExecutionTargets` 驱动。 -App 侧任务发送只调用 bridge 主入口 `/acp/rpc`,不再拼接 provider-specific 直连 URL。Provider 与 gateway 的实际执行地址是 bridge 内部运行时事实,不属于 App contract。 +App 侧任务发送默认调用 bridge 主入口 `/acp/rpc`,不再拼接 provider-specific 直连 URL。OpenClaw `session.start` 和同一任务的 `session.message` 是唯一例外,使用 bridge 公开的 task submit 专用路径 `/gateway/openclaw`。该路径不是全局 ACP base endpoint。 ## App Runtime Flow @@ -22,8 +22,11 @@ flowchart TD D --> J["OpenClaw"] A --> P["POST https://xworkmate-bridge.svc.plus/acp/rpc"] + A --> T["OpenClaw task POST /gateway/openclaw"] P --> Q["Authorization: Bearer token"] + T --> Q P --> R["provider / requestedExecutionTarget params"] + T --> R R --> S["bridge-owned routing"] S --> K["Hermes internal runtime"] @@ -35,9 +38,10 @@ flowchart TD ## Routing Rules -- App runtime requests use `https://xworkmate-bridge.svc.plus/acp/rpc`. +- App runtime control-plane requests, agent tasks, multi-agent tasks, `session.cancel`, and `session.close` use `https://xworkmate-bridge.svc.plus/acp/rpc`. +- OpenClaw gateway `session.start` and follow-up `session.message` use `https://xworkmate-bridge.svc.plus/gateway/openclaw`. - Provider and gateway selection are passed as request params, including `provider`, `routing`, and `requestedExecutionTarget`. - Bridge-owned internal routing is opaque to the App; it is not represented as public provider paths. - The app must not route managed bridge tasks to local or LAN endpoints such as `127.0.0.1:*` or `192.168.*:*`. -- The app must not route managed bridge tasks by directly constructing `/acp-server/*` or `/gateway/*` URLs. -- All App-side requests go through `https://xworkmate-bridge.svc.plus/acp/rpc`. +- The app must not route managed bridge tasks by directly constructing `/acp-server/*` URLs. +- `/gateway/openclaw` is allowed only for OpenClaw task submit; it must not be reused for capabilities, routing, gateway control-plane, cancel, close, or as an ACP base endpoint. diff --git a/docs/architecture/unified-routing-architecture.md b/docs/architecture/unified-routing-architecture.md index 39673cf2..18732515 100644 --- a/docs/architecture/unified-routing-architecture.md +++ b/docs/architecture/unified-routing-architecture.md @@ -2,7 +2,7 @@ ## 1. 架构概览 (Unified Routing Architecture) -当前系统采用 `xworkmate-bridge.svc.plus` 作为统一入口。App 侧只通过 managed bridge ACP 主入口发送任务,provider / gateway 的执行地址由 bridge 后端内部拥有,不暴露为 App-facing public mapping。 +当前系统采用 `xworkmate-bridge.svc.plus` 作为统一入口。App 侧通过 managed bridge ACP 主入口处理能力发现、路由解析、agent / multi-agent 任务和会话控制;OpenClaw `session.start` / `session.message` 使用 bridge 暴露的 `/gateway/openclaw` task submit 专用入口。Provider runtime 地址仍由 bridge 后端内部拥有,不暴露为 App-facing public mapping。 ```mermaid graph TD @@ -16,6 +16,7 @@ graph TD subgraph "Bridge-owned Routing" ManagedBridge["Managed Bridge ACP
/acp/rpc"] + OpenClawSubmit["OpenClaw task submit
/gateway/openclaw"] CodexProvider["Codex internal runtime"] OpenCodeProvider["OpenCode internal runtime"] GeminiAdapter["Gemini internal runtime"] @@ -26,10 +27,11 @@ graph TD Client -->|HTTPS/WSS| Bridge_Domain Bridge_Domain -->|/acp/rpc| ManagedBridge + Bridge_Domain -->|/gateway/openclaw| OpenClawSubmit ManagedBridge -->|provider routing| CodexProvider ManagedBridge -->|provider routing| OpenCodeProvider ManagedBridge -->|provider routing| GeminiAdapter - ManagedBridge -->|gateway routing| OpenClawGateway + OpenClawSubmit -->|forced openclaw routing| OpenClawGateway %% Service Connections ManagedBridge -.->|Capabilities Discovery| Client @@ -39,14 +41,15 @@ graph TD | Bridge-owned mapping | App 侧行为 | 备注 | | :--- | :--- | :--- | -| `/acp/rpc` | 直接调用 | Managed Bridge ACP 主入口,提供能力发现与任务发送 | +| `/acp/rpc` | 直接调用 | 能力发现、路由解析、agent / multi-agent 任务、cancel、close | +| `/gateway/openclaw` | 仅 OpenClaw task submit | 只用于 OpenClaw `session.start` / `session.message`,不是 ACP base endpoint | | provider runtime | 不直连 | Bridge 后端内部解析 provider | | gateway runtime | 不直连 | Bridge 后端内部解析 gateway provider | ## 3. 运维配置优化 ### 3.1 统一鉴权 -App 发往 `xworkmate-bridge.svc.plus/acp/rpc` 的请求必须携带: +App 发往 `xworkmate-bridge.svc.plus/acp/rpc` 和 `xworkmate-bridge.svc.plus/gateway/openclaw` 的请求必须携带: - **Header**: `Authorization: Bearer ` - **未授权响应**: `401 Unauthorized` @@ -62,5 +65,6 @@ App 发往 `xworkmate-bridge.svc.plus/acp/rpc` 的请求必须携带: ## 4. App 侧不变量 - App 不写入或拼接本地 provider endpoint。 -- App 不直接调用 `/acp-server/*` 或 `/gateway/openclaw`。 +- App 不直接调用 `/acp-server/*`。 +- App 仅可在 OpenClaw `session.start` / `session.message` task submit 中调用 `/gateway/openclaw`,不得把它作为全局 ACP base endpoint。 - `acp.capabilities` 是 provider catalog、gateway catalog、available execution targets 的唯一来源。 diff --git a/docs/testing/app-external-service-api-test-matrix.md b/docs/testing/app-external-service-api-test-matrix.md index 5130a4db..6028eee4 100644 --- a/docs/testing/app-external-service-api-test-matrix.md +++ b/docs/testing/app-external-service-api-test-matrix.md @@ -40,7 +40,7 @@ Last Updated: 2026-04-22 - 安全边界确认: - `BRIDGE_AUTH_TOKEN` 是否只作为 Bearer token 使用 - `BRIDGE_SERVER_URL` 是否仅作为元数据 - - 会话接口是否仍通过统一 `/acp/rpc` 入口 + - 会话接口是否按当前 bridge contract 选择 `/acp/rpc` 或 OpenClaw task submit 专用入口 `/gateway/openclaw` ## 2. 统一测试前提 @@ -76,7 +76,8 @@ Last Updated: 2026-04-22 | accounts | `/api/auth/login` | `POST` | 无 | 登录并拿 session token | Apple 审核只读账号使用 | | accounts | `/api/auth/session` | `GET` | `Authorization: Bearer ` | 获取当前会话和用户信息 | APP 端登录态校验 | | accounts | `/api/auth/xworkmate/profile/sync` | `GET` | `Authorization: Bearer ` | 拉取 bridge 同步元数据 | 返回 `BRIDGE_SERVER_URL` / `BRIDGE_AUTH_TOKEN` | -| bridge | `/acp/rpc` | `POST` | `Authorization: Bearer ` | bridge JSON-RPC 主入口 | 所有运行时任务统一走这里 | +| bridge | `/acp/rpc` | `POST` | `Authorization: Bearer ` | bridge JSON-RPC 主入口 | capabilities、routing、agent / multi-agent 任务、cancel、close | +| bridge | `/gateway/openclaw` | `POST` | `Authorization: Bearer ` | OpenClaw task submit 专用入口 | 仅 OpenClaw `session.start` / `session.message`,不是全局 ACP base endpoint | | bridge | `acp.capabilities` | JSON-RPC method | 同上 | 拉取 provider catalog / target catalog | capability 只读快照 | | bridge | `xworkmate.routing.resolve` | JSON-RPC method | 同上 | 解析 provider / gateway / skills 路由 | 返回 resolved / unavailable 信息 | | bridge | `session.start` | JSON-RPC method | 同上 | 开启新会话 | session 生命周期起点 | @@ -580,8 +581,10 @@ Last Updated: 2026-04-22 ### 8.3 建议断言 - `Authorization` 必须是 `Bearer ` -- 请求路径必须是 `/acp/rpc` -- 不允许出现 `/acp-server/` 或 `/gateway/` 直连请求 +- capability、routing、agent / multi-agent 任务、`session.cancel`、`session.close` 请求路径必须是 `/acp/rpc` +- OpenClaw gateway `session.start` / `session.message` 请求路径必须是 `/gateway/openclaw` +- 不允许出现 `/acp-server/` 直连请求 +- `/gateway/openclaw` 不得用于 capabilities、routing、gateway control-plane、cancel、close,且不得作为全局 ACP base endpoint - `session.cancel` / `session.close` 必须接受 `sessionId` + `threadId` - `BRIDGE_SERVER_URL` 不可参与运行时路径拼接 @@ -609,7 +612,7 @@ Last Updated: 2026-04-22 - 账户侧负责登录与同步元数据 - bridge 侧负责 capability、路由解析和会话生命周期 -- 任务执行必须统一经过 `/acp/rpc` -- `session.start` / `session.message` 的协议入口已验证通畅 +- capability、routing、agent / multi-agent 任务、cancel、close 经过 `/acp/rpc` +- OpenClaw `session.start` / `session.message` 经过 `/gateway/openclaw` - `session.cancel` / `session.close` 的协议入口已验证可用 - 当前剩余风险集中在桥接后端下游 provider 连接,而不是 APP 侧接口拼接 diff --git a/lib/app/app_controller_desktop_runtime_helpers.dart b/lib/app/app_controller_desktop_runtime_helpers.dart index 72450427..e349f517 100644 --- a/lib/app/app_controller_desktop_runtime_helpers.dart +++ b/lib/app/app_controller_desktop_runtime_helpers.dart @@ -867,6 +867,9 @@ extension AppControllerDesktopRuntimeHelpers on AppController { if (bridgeEndpoint == null) { return null; } + if (_usesOpenClawTaskSubmitEndpointInternal(request)) { + return bridgeEndpoint.replace(path: '/gateway/openclaw'); + } return bridgeEndpoint; } @@ -967,6 +970,14 @@ extension AppControllerDesktopRuntimeHelpers on AppController { ) => kGatewayRemoteProfileIndex; } +bool _usesOpenClawTaskSubmitEndpointInternal(GoTaskServiceRequest request) { + if (request.isMultiAgentRequest || !request.target.isGateway) { + return false; + } + return normalizeSingleAgentProviderId(request.provider.providerId) == + kCanonicalGatewayProviderId; +} + String _normalizeAuthorizationHeaderInternal(String raw) { final trimmed = raw.trim(); if (trimmed.isEmpty) { diff --git a/lib/runtime/gateway_acp_client.dart b/lib/runtime/gateway_acp_client.dart index 0ea68689..ef095815 100644 --- a/lib/runtime/gateway_acp_client.dart +++ b/lib/runtime/gateway_acp_client.dart @@ -566,7 +566,7 @@ class GatewayAcpClient { ); httpRequest.headers.set( HttpHeaders.acceptHeader, - 'text/event-stream, application/json', + _httpAcceptHeaderFor(endpoint, request.method), ); final authorization = await _resolveAuthorizationHeader( endpoint, @@ -1049,6 +1049,14 @@ class GatewayAcpClient { Uri? _resolveHttpRpcEndpoint([Uri? endpointOverride, String method = '']) { final endpoint = endpointOverride ?? endpointResolver(); + if (_isOpenClawTaskSubmitEndpoint(endpoint) && + _isOpenClawTaskSubmitMethod(method)) { + return endpoint?.replace( + path: '/gateway/openclaw', + query: null, + fragment: null, + ); + } return resolveAcpHttpRpcEndpoint(endpoint); } @@ -1107,7 +1115,33 @@ class GatewayAcpClient { } } +bool _isOpenClawTaskSubmitEndpoint(Uri? endpoint) { + var path = endpoint?.path.trim() ?? ''; + if (!path.startsWith('/')) { + path = '/$path'; + } + path = path.replaceFirst(RegExp(r'/+$'), ''); + return path == '/gateway/openclaw'; +} + +bool _isOpenClawTaskSubmitMethod(String method) { + final normalized = method.trim(); + return normalized == 'session.start' || normalized == 'session.message'; +} + +String _httpAcceptHeaderFor(Uri endpoint, String method) { + if (_isOpenClawTaskSubmitEndpoint(endpoint) && + _isOpenClawTaskSubmitMethod(method)) { + return 'application/json'; + } + return 'text/event-stream, application/json'; +} + Duration gatewayAcpHttpResponseTimeoutFor(Uri endpoint, String method) { + if (_isOpenClawTaskSubmitEndpoint(endpoint) && + _isOpenClawTaskSubmitMethod(method)) { + return const Duration(minutes: 10); + } return const Duration(seconds: 120); } diff --git a/test/runtime/gateway_acp_client_auth_test.dart b/test/runtime/gateway_acp_client_auth_test.dart index 18443fdf..719a6fb9 100644 --- a/test/runtime/gateway_acp_client_auth_test.dart +++ b/test/runtime/gateway_acp_client_auth_test.dart @@ -687,160 +687,193 @@ void main() { }, ); - test('desktop task execution routes OpenClaw through bridge RPC', () async { - final capture = await _startAcpHttpServer(); - addTearDown(capture.close); - final client = GatewayAcpClient( - endpointResolver: () => capture.baseEndpoint, - authorizationResolver: (_) async => 'bridge-token', - ); - - final transport = ExternalCodeAgentAcpDesktopTransport( - client: client, - endpointResolver: (_) => capture.baseEndpoint, - taskEndpointResolver: (_) => capture.baseEndpoint, - ); - - await transport.executeTask( - _taskRequest( - target: AssistantExecutionTarget.gateway, - provider: SingleAgentProvider.openclaw, - ), - onUpdate: (_) {}, - ); - - expect(capture.authorizationHeader, 'Bearer bridge-token'); - expect(capture.acceptHeader, 'text/event-stream, application/json'); - expect(capture.requestPath, '/acp/rpc'); - expect(capture.requestPath, isNot(contains('/acp-server'))); - expect(capture.requestPath, isNot(contains('/acp-server/gateway'))); - expect(capture.requestPath, isNot(contains('/gateway/openclaw'))); - final params = _lastRequestParams(capture); - final routing = params['routing'] as Map; - expect(params.containsKey('gatewayProvider'), isFalse); - expect(params.containsKey('gatewayProviderId'), isFalse); - expect(params['executionTarget'], 'gateway'); - expect(params['requestedExecutionTarget'], 'gateway'); - expect(routing['preferredGatewayProviderId'], 'openclaw'); - expect(routing['explicitExecutionTarget'], 'gateway'); - expect(routing.containsKey('explicitProviderId'), isFalse); - expect(capture.requestBody, contains('"method":"session.start"')); - expect(capture.requestBody, isNot(contains('"method":"thread/start"'))); - }); - - test('desktop OpenClaw follow-up routes through bridge RPC', () async { - final capture = await _startAcpHttpServer(); - addTearDown(capture.close); - final client = GatewayAcpClient( - endpointResolver: () => capture.baseEndpoint, - authorizationResolver: (_) async => 'bridge-token', - ); - - final transport = ExternalCodeAgentAcpDesktopTransport( - client: client, - endpointResolver: (_) => capture.baseEndpoint, - taskEndpointResolver: (_) => capture.baseEndpoint, - ); - - await transport.executeTask( - _taskRequest( - target: AssistantExecutionTarget.gateway, - provider: SingleAgentProvider.openclaw, - resumeSession: true, - ), - onUpdate: (_) {}, - ); - - expect(capture.acceptHeader, 'text/event-stream, application/json'); - expect(capture.requestPath, '/acp/rpc'); - expect(capture.requestPath, isNot(contains('/gateway/openclaw'))); - expect(capture.requestBody, contains('"method":"session.message"')); - }); - test( - 'bridge RPC session methods use the standard HTTP response timeout', - () { - final openClawEndpoint = Uri.parse( - 'https://xworkmate-bridge.svc.plus/gateway/openclaw', - ); - final acpEndpoint = Uri.parse( - 'https://xworkmate-bridge.svc.plus/acp/rpc', + 'desktop task execution rejects OpenClaw gateway path for non-task methods', + () async { + final capture = await _startAcpHttpServer(); + addTearDown(capture.close); + final client = GatewayAcpClient( + endpointResolver: () => + capture.baseEndpoint.replace(path: '/gateway/openclaw'), + authorizationResolver: (_) async => 'bridge-token', ); - expect( - gatewayAcpHttpResponseTimeoutFor(openClawEndpoint, 'session.start'), - const Duration(seconds: 120), - ); - expect( - gatewayAcpHttpResponseTimeoutFor(openClawEndpoint, 'session.message'), - const Duration(seconds: 120), - ); - expect( - gatewayAcpHttpResponseTimeoutFor(acpEndpoint, 'session.start'), - const Duration(seconds: 120), - ); - expect( - gatewayAcpHttpResponseTimeoutFor( - openClawEndpoint, - 'acp.capabilities', + await expectLater( + client.request( + method: 'acp.capabilities', + params: const {}, + ), + throwsA( + isA().having( + (error) => error.code, + 'code', + 'ACP_HTTP_ENDPOINT_MISSING', + ), ), - const Duration(seconds: 120), ); + + expect(capture.requestBodies, isEmpty); }, ); - test('desktop controller resolves task requests to the bridge origin', () { - final controller = AppController( - environmentOverride: const {}, - ); - addTearDown(controller.dispose); + test( + 'desktop task execution routes OpenClaw through dedicated bridge gateway path', + () async { + final capture = await _startAcpHttpServer(); + addTearDown(capture.close); + final client = GatewayAcpClient( + endpointResolver: () => capture.baseEndpoint, + authorizationResolver: (_) async => 'bridge-token', + ); - final openClawStart = controller - .resolveExternalAcpEndpointForRequestInternal( - _taskRequest( - target: AssistantExecutionTarget.gateway, - provider: SingleAgentProvider.openclaw, - ), - ); - final openClawFollowUp = controller - .resolveExternalAcpEndpointForRequestInternal( - _taskRequest( - target: AssistantExecutionTarget.gateway, - provider: SingleAgentProvider.openclaw, - resumeSession: true, - ), - ); - final unspecifiedGateway = controller - .resolveExternalAcpEndpointForRequestInternal( - _taskRequest( - target: AssistantExecutionTarget.gateway, - provider: SingleAgentProvider.unspecified, - ), - ); - final multiAgentGateway = controller - .resolveExternalAcpEndpointForRequestInternal( - _taskRequest( - target: AssistantExecutionTarget.gateway, - provider: SingleAgentProvider.openclaw, - multiAgent: true, - ), - ); - final agentTask = controller.resolveExternalAcpEndpointForRequestInternal( - _taskRequest( - target: AssistantExecutionTarget.agent, - provider: SingleAgentProvider.codex, - ), + final transport = ExternalCodeAgentAcpDesktopTransport( + client: client, + endpointResolver: (_) => capture.baseEndpoint, + taskEndpointResolver: (_) => + capture.baseEndpoint.replace(path: '/gateway/openclaw'), + ); + + await transport.executeTask( + _taskRequest( + target: AssistantExecutionTarget.gateway, + provider: SingleAgentProvider.openclaw, + ), + onUpdate: (_) {}, + ); + + expect(capture.authorizationHeader, 'Bearer bridge-token'); + expect(capture.acceptHeader, 'application/json'); + expect(capture.requestPath, '/gateway/openclaw'); + expect(capture.requestPath, isNot(contains('/acp-server'))); + expect(capture.requestPath, isNot(contains('/acp-server/gateway'))); + final params = _lastRequestParams(capture); + final routing = params['routing'] as Map; + expect(params.containsKey('gatewayProvider'), isFalse); + expect(params.containsKey('gatewayProviderId'), isFalse); + expect(params['executionTarget'], 'gateway'); + expect(params['requestedExecutionTarget'], 'gateway'); + expect(routing['preferredGatewayProviderId'], 'openclaw'); + expect(routing['explicitExecutionTarget'], 'gateway'); + expect(routing.containsKey('explicitProviderId'), isFalse); + expect(capture.requestBody, contains('"method":"session.start"')); + expect(capture.requestBody, isNot(contains('"method":"thread/start"'))); + }, + ); + + test( + 'desktop OpenClaw follow-up routes through dedicated bridge gateway path', + () async { + final capture = await _startAcpHttpServer(); + addTearDown(capture.close); + final client = GatewayAcpClient( + endpointResolver: () => capture.baseEndpoint, + authorizationResolver: (_) async => 'bridge-token', + ); + + final transport = ExternalCodeAgentAcpDesktopTransport( + client: client, + endpointResolver: (_) => capture.baseEndpoint, + taskEndpointResolver: (_) => + capture.baseEndpoint.replace(path: '/gateway/openclaw'), + ); + + await transport.executeTask( + _taskRequest( + target: AssistantExecutionTarget.gateway, + provider: SingleAgentProvider.openclaw, + resumeSession: true, + ), + onUpdate: (_) {}, + ); + + expect(capture.acceptHeader, 'application/json'); + expect(capture.requestPath, '/gateway/openclaw'); + expect(capture.requestBody, contains('"method":"session.message"')); + }, + ); + + test('OpenClaw task submit uses extended HTTP response timeout', () { + final openClawEndpoint = Uri.parse( + 'https://xworkmate-bridge.svc.plus/gateway/openclaw', + ); + final acpEndpoint = Uri.parse( + 'https://xworkmate-bridge.svc.plus/acp/rpc', ); - expect(openClawStart?.path, ''); - expect(openClawFollowUp?.path, ''); - expect(unspecifiedGateway?.path, ''); - expect(multiAgentGateway?.path, ''); - expect(agentTask?.path, ''); + expect( + gatewayAcpHttpResponseTimeoutFor(openClawEndpoint, 'session.start'), + const Duration(minutes: 10), + ); + expect( + gatewayAcpHttpResponseTimeoutFor(openClawEndpoint, 'session.message'), + const Duration(minutes: 10), + ); + expect( + gatewayAcpHttpResponseTimeoutFor(acpEndpoint, 'session.start'), + const Duration(seconds: 120), + ); + expect( + gatewayAcpHttpResponseTimeoutFor(openClawEndpoint, 'acp.capabilities'), + const Duration(seconds: 120), + ); }); test( - 'desktop controller does not expose OpenClaw gateway path as task endpoint', + 'desktop controller only uses gateway path for OpenClaw task submit', + () { + final controller = AppController( + environmentOverride: const {}, + ); + addTearDown(controller.dispose); + + final openClawStart = controller + .resolveExternalAcpEndpointForRequestInternal( + _taskRequest( + target: AssistantExecutionTarget.gateway, + provider: SingleAgentProvider.openclaw, + ), + ); + final openClawFollowUp = controller + .resolveExternalAcpEndpointForRequestInternal( + _taskRequest( + target: AssistantExecutionTarget.gateway, + provider: SingleAgentProvider.openclaw, + resumeSession: true, + ), + ); + final unspecifiedGateway = controller + .resolveExternalAcpEndpointForRequestInternal( + _taskRequest( + target: AssistantExecutionTarget.gateway, + provider: SingleAgentProvider.unspecified, + ), + ); + final multiAgentGateway = controller + .resolveExternalAcpEndpointForRequestInternal( + _taskRequest( + target: AssistantExecutionTarget.gateway, + provider: SingleAgentProvider.openclaw, + multiAgent: true, + ), + ); + final agentTask = controller + .resolveExternalAcpEndpointForRequestInternal( + _taskRequest( + target: AssistantExecutionTarget.agent, + provider: SingleAgentProvider.codex, + ), + ); + + expect(openClawStart?.path, '/gateway/openclaw'); + expect(openClawFollowUp?.path, '/gateway/openclaw'); + expect(unspecifiedGateway?.path, ''); + expect(multiAgentGateway?.path, ''); + expect(agentTask?.path, ''); + }, + ); + + test( + 'desktop controller resolves OpenClaw gateway submit on managed bridge origin', () { final controller = AppController( environmentOverride: const {}, @@ -855,9 +888,12 @@ void main() { ), ); - expect(endpoint.toString(), 'https://xworkmate-bridge.svc.plus'); + expect( + endpoint.toString(), + 'https://xworkmate-bridge.svc.plus/gateway/openclaw', + ); expect(endpoint, isNotNull); - expect(endpoint!.path, isNot('/gateway/openclaw')); + expect(endpoint!.path, isNot('/acp/rpc')); }, );