Enforce bridge-only production routing in app
This commit is contained in:
parent
e7a3eecfa6
commit
483663c598
@ -5,8 +5,7 @@ with the provider catalog aligned to the bridge-only design.
|
||||
|
||||
## Current Rule
|
||||
|
||||
- Settings only manages bridge connection parameters and upstream sync
|
||||
definitions.
|
||||
- Settings only manages bridge connection parameters and account sync metadata.
|
||||
- The provider picker is not derived from local endpoint presets.
|
||||
- `xworkmate-bridge` is the only source of truth for the provider catalog.
|
||||
|
||||
@ -16,15 +15,7 @@ with the provider catalog aligned to the bridge-only design.
|
||||
flowchart TD
|
||||
A["Settings UI
|
||||
仅管理 Bridge 连接参数
|
||||
与自定义 upstream sync 定义"] --> B["SettingsSnapshot.externalAcpEndpoints
|
||||
仅作为 sync 输入"]
|
||||
|
||||
B --> C["buildExternalAcpSyncedProvidersInternal()"]
|
||||
C --> D["syncExternalAcpProvidersInternal()"]
|
||||
D --> E["xworkmate.providers.sync"]
|
||||
E --> F["xworkmate-bridge providerCatalog"]
|
||||
|
||||
F --> G["acp.capabilities"]
|
||||
与账号同步元数据"] --> G["acp.capabilities"]
|
||||
G --> H["providerCatalog[]
|
||||
singleAgent / multiAgent"]
|
||||
|
||||
@ -77,7 +68,7 @@ flowchart TD
|
||||
|
||||
## Notes
|
||||
|
||||
- `externalAcpEndpoints` still matters, but only as bridge sync input.
|
||||
- Production cloud mode does not use app-side provider sync.
|
||||
- Provider visibility and picker contents come from
|
||||
`acp.capabilities.providerCatalog`.
|
||||
- Auto-provider resolution and unavailable messaging come from
|
||||
|
||||
@ -53,15 +53,7 @@ Single-agent provider catalog and availability are owned by
|
||||
flowchart TD
|
||||
A["Settings UI
|
||||
仅管理 Bridge 连接参数
|
||||
与自定义 upstream sync 定义"] --> B["SettingsSnapshot.externalAcpEndpoints
|
||||
仅作为 sync 输入"]
|
||||
|
||||
B --> C["buildExternalAcpSyncedProvidersInternal()"]
|
||||
C --> D["syncExternalAcpProvidersInternal()"]
|
||||
D --> E["xworkmate.providers.sync"]
|
||||
E --> F["xworkmate-bridge providerCatalog"]
|
||||
|
||||
F --> G["acp.capabilities"]
|
||||
与账号同步元数据"] --> G["acp.capabilities"]
|
||||
G --> H["providerCatalog[]
|
||||
singleAgent / multiAgent"]
|
||||
|
||||
@ -118,6 +110,8 @@ flowchart TD
|
||||
- Desktop App 直接桥接 Go 代码
|
||||
- Desktop 正常执行链路不以“先启动一个本地 HTTP server,再由 Desktop 自己回连”作为目标架构
|
||||
- Desktop 的 `sendMessage -> GoTaskService.executeTask -> ACP` 应理解为进程内或直接桥接语义
|
||||
- Production cloud mode does not call `xworkmate.providers.sync`
|
||||
- Production provider upstreams are bridge-owned, not app-owned
|
||||
- 对 app 来说,bridge 是 discovery / config / connect / dialogue 的统一枢纽
|
||||
|
||||
### Web / Mobile
|
||||
|
||||
@ -6,96 +6,108 @@ Date: 2026-04-11
|
||||
|
||||
Scope:
|
||||
- `xworkmate-app`
|
||||
- Settings / account sync / local UI state / task thread persistence
|
||||
- settings / account sync / cloud runtime state
|
||||
|
||||
## V1 Decision
|
||||
|
||||
This worktree implements the first app-side simplification:
|
||||
Production cloud mode is bridge-only:
|
||||
|
||||
- keep a single persisted config file: `config/settings.yaml`
|
||||
- move local recoverable UI state to `ui/state.json`
|
||||
- keep task title/archive in `tasks/*.json`
|
||||
- make account sync one-way overwrite for sync-owned fields
|
||||
- keep bridge provider catalog / runtime capabilities runtime-only
|
||||
- app-facing cloud endpoint is fixed to `https://xworkmate-bridge.svc.plus`
|
||||
- production provider catalog is bridge-owned
|
||||
- production gateway upstream is bridge-owned
|
||||
- account sync is metadata-only for session state, status, and managed secret references
|
||||
- account sync does not own executable ACP or gateway upstream endpoints
|
||||
|
||||
## Overview Workflow
|
||||
## Production Routing Truth
|
||||
|
||||
The app does not define or sync production upstreams.
|
||||
|
||||
Bridge-owned production routing is:
|
||||
|
||||
- `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`
|
||||
|
||||
The app only talks to:
|
||||
|
||||
- `https://xworkmate-bridge.svc.plus`
|
||||
|
||||
## App Responsibilities
|
||||
|
||||
- sign in to `accounts.svc.plus`
|
||||
- persist account session and sync metadata
|
||||
- call bridge runtime methods:
|
||||
- `acp.capabilities`
|
||||
- `xworkmate.routing.resolve`
|
||||
- `session.start`
|
||||
- `session.message`
|
||||
- `session.cancel`
|
||||
- `session.close`
|
||||
- bridge-owned gateway methods
|
||||
- render bridge/provider/gateway status from bridge runtime results
|
||||
|
||||
## Removed Responsibilities
|
||||
|
||||
- no app-side direct-connect cloud path
|
||||
- no production `xworkmate.providers.sync`
|
||||
- no production provider catalog from `providerSyncDefinitions`
|
||||
- no execution-time use of account-synced `openclawUrl`
|
||||
- no execution-time use of account-synced `apisixUrl`
|
||||
- no direct app calls to `acp-server.svc.plus/*`
|
||||
- no direct app calls to `openclaw.svc.plus`
|
||||
|
||||
## State Rules
|
||||
|
||||
`settings.yaml`
|
||||
|
||||
- stores current user settings and local editing state
|
||||
- does not own production ACP upstream definitions
|
||||
- does not get executable provider endpoints from account sync
|
||||
|
||||
`account/sync_state.json`
|
||||
|
||||
- stores synced account metadata only
|
||||
- may retain `openclawUrl` / `apisixUrl` as account profile metadata
|
||||
- does not overwrite executable cloud routing targets
|
||||
|
||||
`acpBridgeServerModeConfig.cloudSynced.remoteServerSummary.endpoint`
|
||||
|
||||
- represents bridge cloud entry only
|
||||
- fixed to `https://xworkmate-bridge.svc.plus` while signed in and synced
|
||||
- is not an upstream provider URL
|
||||
- is not a gateway upstream URL
|
||||
|
||||
## Workflow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
UI["Settings UI / App Startup"] --> INIT["SettingsController.initialize()"]
|
||||
|
||||
subgraph LocalStores["APP Local Stores"]
|
||||
YAML["config/settings.yaml"]
|
||||
UISTATE["ui/state.json"]
|
||||
SYNCJSON["account/sync_state.json"]
|
||||
SECRET["secrets/*.secret\naccount session token / managed secrets"]
|
||||
TASKS["tasks/*.json\nthread title / archived / thread-owned state"]
|
||||
end
|
||||
|
||||
INIT --> LOAD["SecureConfigStore.loadSettingsSnapshot()"]
|
||||
LOAD --> YAML
|
||||
|
||||
INIT --> LOADUI["SecureConfigStore.loadAppUiState()"]
|
||||
LOADUI --> UISTATE
|
||||
|
||||
INIT --> LOADTHREADS["loadTaskThreads()"]
|
||||
LOADTHREADS --> TASKS
|
||||
|
||||
INIT --> LOAD["load settings + UI state + task state"]
|
||||
INIT --> RESTORE["restoreAccountSession()"]
|
||||
RESTORE --> TOKEN["loadAccountSessionToken()"]
|
||||
TOKEN --> SECRET
|
||||
|
||||
TOKEN --> CHECK{"baseUrl + session token ready?"}
|
||||
CHECK -->|no| BLOCK["blocked\nAccount session is unavailable"]
|
||||
RESTORE --> CHECK{"account session ready?"}
|
||||
CHECK -->|no| BLOCK["blocked"]
|
||||
CHECK -->|yes| SYNC["syncAccountSettingsInternal(baseUrl)"]
|
||||
|
||||
SYNC --> API["AccountRuntimeClient.loadProfile(token)"]
|
||||
API --> SAVE_SYNC["saveAccountSyncState(nextState)"]
|
||||
SAVE_SYNC --> SYNCJSON
|
||||
|
||||
API --> MODECFG["saveSnapshot(\naccountLocalMode=false,\nacpBridgeServerModeConfig.cloudSynced=remote summary\n)"]
|
||||
MODECFG --> YAML
|
||||
|
||||
API --> SAVE_SYNC["save account sync metadata"]
|
||||
API --> SAVE_SUMMARY["set cloud summary endpoint = bridge base URL"]
|
||||
API --> APPLY["applyAccountSyncedDefaultsSettingsInternal(state)"]
|
||||
|
||||
APPLY --> O1["overwrite remote gateway endpoint"]
|
||||
APPLY --> O2["overwrite gateway tokenRef"]
|
||||
APPLY --> O3["overwrite vault address / namespace"]
|
||||
APPLY --> O4["overwrite aiGateway baseUrl / apiKeyRef"]
|
||||
APPLY --> O5["overwrite ollamaCloud apiKeyRef"]
|
||||
APPLY --> O6["update cloudSynced metadata"]
|
||||
APPLY --> KEEP1["keep vault metadata"]
|
||||
APPLY --> KEEP2["keep managed secret refs"]
|
||||
APPLY --> SKIP1["do not overwrite gateway executable endpoint"]
|
||||
APPLY --> SKIP2["do not overwrite ACP executable endpoint"]
|
||||
|
||||
O1 --> SAVE["saveSnapshot(next settings)"]
|
||||
O2 --> SAVE
|
||||
O3 --> SAVE
|
||||
O4 --> SAVE
|
||||
O5 --> SAVE
|
||||
O6 --> SAVE
|
||||
|
||||
SAVE --> YAML
|
||||
SAVE --> DERIVED["reloadDerivedStateInternal()"]
|
||||
DERIVED --> VIEW["Settings / Runtime ViewModel"]
|
||||
|
||||
VIEW --> NOTE1["does not auto-connect gateway"]
|
||||
APPLY -. not touched .-> NOTE2["providerSyncDefinitions\n(sync payload definitions)\nnot overwritten here"]
|
||||
|
||||
UI --> LOCAL_EDIT["local settings edit"]
|
||||
LOCAL_EDIT --> SAVE_LOCAL["saveSnapshot()"]
|
||||
SAVE_LOCAL --> YAML
|
||||
|
||||
UI --> UI_EDIT["local ui restore edit"]
|
||||
UI_EDIT --> SAVE_UI["saveAppUiState()"]
|
||||
SAVE_UI --> UISTATE
|
||||
|
||||
UI --> THREAD_EDIT["rename / archive / restore thread"]
|
||||
THREAD_EDIT --> SAVE_THREAD["saveTaskThreads()"]
|
||||
SAVE_THREAD --> TASKS
|
||||
UI --> BRIDGE_CAPS["acp.capabilities via bridge"]
|
||||
UI --> BRIDGE_ROUTE["xworkmate.routing.resolve via bridge"]
|
||||
UI --> BRIDGE_RUN["session.* via bridge"]
|
||||
UI --> BRIDGE_GATEWAY["xworkmate.gateway.* via bridge"]
|
||||
```
|
||||
|
||||
## V1 Boundaries
|
||||
## Invariants
|
||||
|
||||
- `settings.yaml` only stores current schema V1 config intent and sync-owned local snapshots.
|
||||
- `ui/state.json` stores `assistantLastSessionKey`, `assistantNavigationDestinations`, and `savedGatewayTargets`.
|
||||
- `tasks/*.json` stores thread-owned display facts such as `title` and `archived`.
|
||||
- `account/sync_state.json` stores sync metadata only, not local override policy.
|
||||
- bridge-advertised providers and ACP capability state stay runtime-only.
|
||||
- `providerSyncDefinitions` is not a production truth source.
|
||||
- account sync may update metadata, but not production execution targets.
|
||||
- gateway runtime status shown in the app must come from bridge runtime results.
|
||||
- bridge capability/provider availability shown in the app must come from `acp.capabilities`.
|
||||
|
||||
@ -300,7 +300,8 @@ class AppController extends ChangeNotifier {
|
||||
late final MultiAgentOrchestrator multiAgentOrchestratorInternal;
|
||||
late final MultiAgentMountManager multiAgentMountManagerInternal;
|
||||
|
||||
GoTaskServiceClient get goTaskServiceClientForTest => goTaskServiceClientInternal;
|
||||
GoTaskServiceClient get goTaskServiceClientForTest =>
|
||||
goTaskServiceClientInternal;
|
||||
|
||||
Map<SingleAgentProvider, SingleAgentCapabilities>
|
||||
singleAgentCapabilitiesByProviderInternal =
|
||||
@ -654,10 +655,7 @@ class AppController extends ChangeNotifier {
|
||||
if (selection == SingleAgentProvider.auto) {
|
||||
return null;
|
||||
}
|
||||
final resolvedSelection = settings.resolveSingleAgentProvider(selection);
|
||||
return canUseSingleAgentProviderInternal(resolvedSelection)
|
||||
? resolvedSelection
|
||||
: null;
|
||||
return canUseSingleAgentProviderInternal(selection) ? selection : null;
|
||||
}
|
||||
|
||||
List<String> get aiGatewayConversationModelChoices {
|
||||
|
||||
@ -57,7 +57,6 @@ Future<void> refreshAcpCapabilitiesRuntimeInternal(
|
||||
controller.sessionsControllerInternal.currentSessionKey,
|
||||
);
|
||||
if (target == AssistantExecutionTarget.singleAgent) {
|
||||
await controller.syncExternalAcpProvidersInternal();
|
||||
await controller.goTaskServiceClientInternal.loadExternalAcpCapabilities(
|
||||
target: AssistantExecutionTarget.singleAgent,
|
||||
forceRefresh: forceRefresh,
|
||||
@ -94,7 +93,6 @@ Future<void> refreshSingleAgentCapabilitiesRuntimeInternal(
|
||||
AppController controller, {
|
||||
bool forceRefresh = false,
|
||||
}) async {
|
||||
await controller.syncExternalAcpProvidersInternal();
|
||||
final capabilities = await controller.goTaskServiceClientInternal
|
||||
.loadExternalAcpCapabilities(
|
||||
target: AssistantExecutionTarget.singleAgent,
|
||||
@ -221,40 +219,15 @@ String? resolveSingleAgentWorkingDirectoryForSessionRuntimeInternal(
|
||||
}
|
||||
|
||||
bool singleAgentProviderRequiresLocalPathRuntimeInternal(
|
||||
AppController controller,
|
||||
AppController _,
|
||||
SingleAgentProvider provider,
|
||||
) {
|
||||
final configuredEndpoint = controller.settings
|
||||
.providerSyncDefinitionForProvider(provider)
|
||||
.endpoint
|
||||
.trim();
|
||||
if (configuredEndpoint.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
final normalizedInput = configuredEndpoint.contains('://')
|
||||
? configuredEndpoint
|
||||
: 'ws://$configuredEndpoint';
|
||||
final endpoint = Uri.tryParse(normalizedInput);
|
||||
if (endpoint == null) {
|
||||
return true;
|
||||
}
|
||||
final scheme = endpoint.scheme.trim().toLowerCase();
|
||||
if (scheme == 'wss' || scheme == 'https') {
|
||||
if (provider == SingleAgentProvider.codex ||
|
||||
provider == SingleAgentProvider.opencode ||
|
||||
provider == SingleAgentProvider.gemini) {
|
||||
return false;
|
||||
}
|
||||
final host = endpoint.host.trim();
|
||||
if (host.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
final address = InternetAddress.tryParse(host);
|
||||
if (address != null) {
|
||||
return !(address.isLoopback || address.type == InternetAddressType.unix);
|
||||
}
|
||||
final normalizedHost = host.toLowerCase();
|
||||
if (normalizedHost == 'localhost') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
CodeAgentNodeState buildCodeAgentNodeStateRuntimeInternal(
|
||||
|
||||
@ -661,42 +661,6 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<List<ExternalCodeAgentAcpSyncedProvider>>
|
||||
buildExternalAcpSyncedProvidersInternal() async {
|
||||
final providers = <ExternalCodeAgentAcpSyncedProvider>[];
|
||||
for (final profile in settings.providerSyncDefinitions) {
|
||||
final provider = settings.singleAgentProviderForId(profile.providerKey);
|
||||
if (provider == SingleAgentProvider.auto) {
|
||||
continue;
|
||||
}
|
||||
final endpoint = profile.endpoint.trim();
|
||||
if (!profile.enabled || endpoint.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
final authorizationHeader = profile.authRef.trim().isEmpty
|
||||
? ''
|
||||
: await settingsControllerInternal.resolveSecretValueInternal(
|
||||
refName: profile.authRef.trim(),
|
||||
);
|
||||
providers.add(
|
||||
ExternalCodeAgentAcpSyncedProvider(
|
||||
providerId: provider.providerId,
|
||||
label: provider.label,
|
||||
endpoint: endpoint,
|
||||
authorizationHeader: authorizationHeader,
|
||||
enabled: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
return providers;
|
||||
}
|
||||
|
||||
Future<void> syncExternalAcpProvidersInternal() async {
|
||||
await goTaskServiceClientInternal.syncExternalProviders(
|
||||
await buildExternalAcpSyncedProvidersInternal(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> persistGoTaskArtifactsForSessionInternal(
|
||||
String sessionKey,
|
||||
GoTaskServiceResult result,
|
||||
|
||||
@ -152,7 +152,6 @@ Future<void> sendSingleAgentMessageDesktopGoTaskFlowInternal(
|
||||
.map((item) => item.label.trim().isNotEmpty ? item.label : item.key)
|
||||
.where((item) => item.trim().isNotEmpty)
|
||||
.toList(growable: false);
|
||||
await controller.syncExternalAcpProvidersInternal();
|
||||
final result = await controller.goTaskServiceClientInternal.executeTask(
|
||||
GoTaskServiceRequest(
|
||||
sessionId: sessionKey,
|
||||
|
||||
@ -306,7 +306,6 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
try {
|
||||
final dispatch = await codeAgentNodeOrchestratorInternal
|
||||
.buildGatewayDispatch(buildCodeAgentNodeStateInternal());
|
||||
await syncExternalAcpProvidersInternal();
|
||||
final result = await goTaskServiceClientInternal.executeTask(
|
||||
GoTaskServiceRequest(
|
||||
sessionId: sessionKey,
|
||||
|
||||
@ -160,7 +160,6 @@ Future<void> runMultiAgentCollaborationThreadSessionInternal(
|
||||
);
|
||||
controller.recomputeTasksInternal();
|
||||
try {
|
||||
await controller.syncExternalAcpProvidersInternal();
|
||||
final result = await controller.goTaskServiceClientInternal.executeTask(
|
||||
GoTaskServiceRequest(
|
||||
sessionId: sessionKey,
|
||||
|
||||
@ -13,8 +13,6 @@ class ExternalCodeAgentAcpDesktopTransport
|
||||
: _bridge = bridge ?? GoAcpStdioBridge();
|
||||
|
||||
final GoAcpStdioBridge _bridge;
|
||||
List<ExternalCodeAgentAcpSyncedProvider> _syncedProviders =
|
||||
const <ExternalCodeAgentAcpSyncedProvider>[];
|
||||
|
||||
@visibleForTesting
|
||||
GoAcpStdioBridge get bridgeForTest => _bridge;
|
||||
@ -22,19 +20,13 @@ class ExternalCodeAgentAcpDesktopTransport
|
||||
@override
|
||||
Future<void> syncExternalProviders(
|
||||
List<ExternalCodeAgentAcpSyncedProvider> providers,
|
||||
) async {
|
||||
_syncedProviders = List<ExternalCodeAgentAcpSyncedProvider>.unmodifiable(
|
||||
providers,
|
||||
);
|
||||
await _syncProviders();
|
||||
}
|
||||
) async {}
|
||||
|
||||
@override
|
||||
Future<ExternalCodeAgentAcpCapabilities> loadExternalAcpCapabilities({
|
||||
required AssistantExecutionTarget target,
|
||||
bool forceRefresh = false,
|
||||
}) async {
|
||||
await _syncProviders();
|
||||
final response = await _bridge.request(
|
||||
method: 'acp.capabilities',
|
||||
params: const <String, dynamic>{},
|
||||
@ -66,7 +58,6 @@ class ExternalCodeAgentAcpDesktopTransport
|
||||
String aiGatewayBaseUrl = '',
|
||||
String aiGatewayApiKey = '',
|
||||
}) async {
|
||||
await _syncProviders();
|
||||
final response = await _bridge.request(
|
||||
method: 'xworkmate.routing.resolve',
|
||||
params: <String, dynamic>{
|
||||
@ -89,7 +80,6 @@ class ExternalCodeAgentAcpDesktopTransport
|
||||
GoTaskServiceRequest request, {
|
||||
required void Function(GoTaskServiceUpdate update) onUpdate,
|
||||
}) async {
|
||||
await _syncProviders();
|
||||
late final StreamSubscription<Map<String, dynamic>> subscription;
|
||||
var streamedText = '';
|
||||
String? completedMessage;
|
||||
@ -158,28 +148,6 @@ class ExternalCodeAgentAcpDesktopTransport
|
||||
@override
|
||||
Future<void> dispose() => _bridge.dispose();
|
||||
|
||||
Future<void> _syncProviders() async {
|
||||
if (_syncedProviders.isEmpty) {
|
||||
return;
|
||||
}
|
||||
await _bridge.request(
|
||||
method: 'xworkmate.providers.sync',
|
||||
params: <String, dynamic>{
|
||||
'providers': _syncedProviders
|
||||
.map(
|
||||
(item) => <String, dynamic>{
|
||||
'providerId': item.providerId,
|
||||
'endpoint': item.endpoint,
|
||||
'label': item.label,
|
||||
'authorizationHeader': item.authorizationHeader,
|
||||
'enabled': item.enabled,
|
||||
},
|
||||
)
|
||||
.toList(growable: false),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _castMap(Object? value) {
|
||||
if (value is Map<String, dynamic>) {
|
||||
return value;
|
||||
|
||||
@ -25,10 +25,7 @@ extension SettingsControllerAccountExtension on SettingsController {
|
||||
if (local.isNotEmpty) {
|
||||
return local;
|
||||
}
|
||||
if (!snapshotInternal.acpBridgeServerModeConfig.usesCloudSyncBase) {
|
||||
return '';
|
||||
}
|
||||
return accountSyncStateInternal?.syncedDefaults.apisixUrl.trim() ?? '';
|
||||
return '';
|
||||
}
|
||||
|
||||
List<String> get effectiveAiGatewayAvailableModels {
|
||||
|
||||
@ -2,6 +2,8 @@ import 'account_runtime_client.dart';
|
||||
import 'runtime_controllers_settings.dart';
|
||||
import 'runtime_models.dart';
|
||||
|
||||
const _kProductionBridgeEndpoint = 'https://xworkmate-bridge.svc.plus';
|
||||
|
||||
Future<void> loginAccountSettingsInternal(
|
||||
SettingsController controller, {
|
||||
required String baseUrl,
|
||||
@ -270,9 +272,7 @@ Future<AccountSyncResult> syncAccountSettingsInternal(
|
||||
lastSyncAt: nextState.lastSyncAtMs,
|
||||
remoteServerSummary: currentModeConfig.cloudSynced.remoteServerSummary
|
||||
.copyWith(
|
||||
endpoint: response.profile.openclawUrl.trim().isNotEmpty
|
||||
? response.profile.openclawUrl.trim()
|
||||
: response.profile.apisixUrl.trim(),
|
||||
endpoint: _kProductionBridgeEndpoint,
|
||||
hasAdvancedOverrides: false,
|
||||
),
|
||||
),
|
||||
@ -352,37 +352,6 @@ Future<void> applyAccountSyncedDefaultsSettingsInternal(
|
||||
final previous = controller.snapshotInternal;
|
||||
var next = previous;
|
||||
final defaults = state.syncedDefaults;
|
||||
if (defaults.openclawUrl.trim().isNotEmpty) {
|
||||
final remoteProfile = previous.gatewayProfiles[kGatewayRemoteProfileIndex];
|
||||
final normalized = normalizeGatewayManualEndpointInternal(
|
||||
host: defaults.openclawUrl,
|
||||
port: remoteProfile.port,
|
||||
tls: remoteProfile.tls,
|
||||
);
|
||||
next = next.copyWithGatewayProfileAt(
|
||||
kGatewayRemoteProfileIndex,
|
||||
remoteProfile.copyWith(
|
||||
mode: RuntimeConnectionMode.remote,
|
||||
useSetupCode: false,
|
||||
setupCode: '',
|
||||
host: normalized.host,
|
||||
port: normalized.port,
|
||||
tls: normalized.tls,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final gatewayTokenLocator = defaults.locatorForTarget(
|
||||
kAccountManagedSecretTargetOpenclawGatewayToken,
|
||||
);
|
||||
if (gatewayTokenLocator != null) {
|
||||
final remoteProfile = next.gatewayProfiles[kGatewayRemoteProfileIndex];
|
||||
next = next.copyWithGatewayProfileAt(
|
||||
kGatewayRemoteProfileIndex,
|
||||
remoteProfile.copyWith(tokenRef: gatewayTokenLocator.target),
|
||||
);
|
||||
}
|
||||
|
||||
if (defaults.vaultUrl.trim().isNotEmpty) {
|
||||
next = next.copyWith(
|
||||
vault: next.vault.copyWith(address: defaults.vaultUrl.trim()),
|
||||
@ -395,12 +364,6 @@ Future<void> applyAccountSyncedDefaultsSettingsInternal(
|
||||
);
|
||||
}
|
||||
|
||||
if (defaults.apisixUrl.trim().isNotEmpty) {
|
||||
next = next.copyWith(
|
||||
aiGateway: next.aiGateway.copyWith(baseUrl: defaults.apisixUrl.trim()),
|
||||
);
|
||||
}
|
||||
|
||||
final aiGatewayLocator = defaults.locatorForTarget(
|
||||
kAccountManagedSecretTargetAIGatewayAccessToken,
|
||||
);
|
||||
@ -433,9 +396,7 @@ Future<void> applyAccountSyncedDefaultsSettingsInternal(
|
||||
.cloudSynced
|
||||
.remoteServerSummary
|
||||
.copyWith(
|
||||
endpoint: defaults.openclawUrl.trim().isNotEmpty
|
||||
? defaults.openclawUrl.trim()
|
||||
: defaults.apisixUrl.trim(),
|
||||
endpoint: _kProductionBridgeEndpoint,
|
||||
hasAdvancedOverrides: false,
|
||||
),
|
||||
),
|
||||
|
||||
@ -208,7 +208,7 @@ SettingsSnapshot _buildCanonicalSettings() {
|
||||
accountIdentifier: 'canonical@svc.plus',
|
||||
lastSyncAt: 123456789,
|
||||
remoteServerSummary: const AcpBridgeServerRemoteServerSummary(
|
||||
endpoint: 'wss://gateway.svc.plus',
|
||||
endpoint: 'https://xworkmate-bridge.svc.plus',
|
||||
hasAdvancedOverrides: false,
|
||||
),
|
||||
),
|
||||
|
||||
@ -73,20 +73,20 @@ void main() {
|
||||
expect(first.state, 'ready');
|
||||
expect(
|
||||
controller.snapshot.gatewayProfiles[kGatewayRemoteProfileIndex].host,
|
||||
'remote.gateway.svc.plus',
|
||||
'local.example.com',
|
||||
);
|
||||
expect(
|
||||
controller
|
||||
.snapshot
|
||||
.gatewayProfiles[kGatewayRemoteProfileIndex]
|
||||
.tokenRef,
|
||||
kAccountManagedSecretTargetOpenclawGatewayToken,
|
||||
'local_ref',
|
||||
);
|
||||
expect(controller.snapshot.vault.address, 'https://vault.svc.plus');
|
||||
expect(controller.snapshot.vault.namespace, 'prod');
|
||||
expect(
|
||||
controller.snapshot.aiGateway.baseUrl,
|
||||
'https://apisix.svc.plus',
|
||||
'https://local-apisix.example.com',
|
||||
);
|
||||
expect(
|
||||
controller.snapshot.aiGateway.apiKeyRef,
|
||||
@ -116,7 +116,16 @@ void main() {
|
||||
expect(controller.snapshot.vault.address, 'https://vault.svc.plus');
|
||||
expect(
|
||||
controller.snapshot.aiGateway.baseUrl,
|
||||
'https://apisix.svc.plus',
|
||||
'https://edited-apisix.example.com',
|
||||
);
|
||||
expect(
|
||||
controller
|
||||
.snapshot
|
||||
.acpBridgeServerModeConfig
|
||||
.cloudSynced
|
||||
.remoteServerSummary
|
||||
.endpoint,
|
||||
'https://xworkmate-bridge.svc.plus',
|
||||
);
|
||||
|
||||
final rawSyncState = await store.loadSupportJson(
|
||||
|
||||
@ -46,48 +46,22 @@ class _FakeGoAcpStdioBridgeWithSyncOrder extends GoAcpStdioBridge {
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('External ACP bridge sync order', () {
|
||||
test('syncs providers before capabilities requests', () async {
|
||||
group('External ACP bridge routing order', () {
|
||||
test('loads capabilities without app-side provider sync', () async {
|
||||
final bridge = _FakeGoAcpStdioBridgeWithSyncOrder();
|
||||
final transport = ExternalCodeAgentAcpDesktopTransport(bridge: bridge);
|
||||
|
||||
await transport
|
||||
.syncExternalProviders(const <ExternalCodeAgentAcpSyncedProvider>[
|
||||
ExternalCodeAgentAcpSyncedProvider(
|
||||
providerId: 'codex',
|
||||
label: 'Codex',
|
||||
endpoint: 'https://acp-server.svc.plus/codex/acp/rpc',
|
||||
authorizationHeader: '',
|
||||
enabled: true,
|
||||
),
|
||||
]);
|
||||
|
||||
await transport.loadExternalAcpCapabilities(
|
||||
target: AssistantExecutionTarget.singleAgent,
|
||||
);
|
||||
|
||||
expect(bridge.methods, <String>[
|
||||
'xworkmate.providers.sync',
|
||||
'xworkmate.providers.sync',
|
||||
'acp.capabilities',
|
||||
]);
|
||||
expect(bridge.methods, <String>['acp.capabilities']);
|
||||
});
|
||||
|
||||
test('syncs providers before session start requests', () async {
|
||||
test('starts sessions without app-side provider sync', () async {
|
||||
final bridge = _FakeGoAcpStdioBridgeWithSyncOrder();
|
||||
final transport = ExternalCodeAgentAcpDesktopTransport(bridge: bridge);
|
||||
|
||||
await transport
|
||||
.syncExternalProviders(const <ExternalCodeAgentAcpSyncedProvider>[
|
||||
ExternalCodeAgentAcpSyncedProvider(
|
||||
providerId: 'codex',
|
||||
label: 'Codex',
|
||||
endpoint: 'https://acp-server.svc.plus/codex/acp/rpc',
|
||||
authorizationHeader: '',
|
||||
enabled: true,
|
||||
),
|
||||
]);
|
||||
|
||||
await transport.executeTask(
|
||||
const GoTaskServiceRequest(
|
||||
sessionId: 's1',
|
||||
@ -108,11 +82,7 @@ void main() {
|
||||
onUpdate: (_) {},
|
||||
);
|
||||
|
||||
expect(bridge.methods, <String>[
|
||||
'xworkmate.providers.sync',
|
||||
'xworkmate.providers.sync',
|
||||
'session.start',
|
||||
]);
|
||||
expect(bridge.methods, <String>['session.start']);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -77,26 +77,23 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'only syncs when app has explicit provider overrides to send',
|
||||
() async {
|
||||
final bridge = _FakeGoAcpStdioBridge();
|
||||
final transport = ExternalCodeAgentAcpDesktopTransport(bridge: bridge);
|
||||
test('ignores app-side provider sync in bridge-only mode', () async {
|
||||
final bridge = _FakeGoAcpStdioBridge();
|
||||
final transport = ExternalCodeAgentAcpDesktopTransport(bridge: bridge);
|
||||
|
||||
await transport
|
||||
.syncExternalProviders(const <ExternalCodeAgentAcpSyncedProvider>[
|
||||
ExternalCodeAgentAcpSyncedProvider(
|
||||
providerId: 'codex',
|
||||
label: 'Codex',
|
||||
endpoint: 'https://acp-server.svc.plus/codex/acp/rpc',
|
||||
authorizationHeader: '',
|
||||
enabled: true,
|
||||
),
|
||||
]);
|
||||
await transport
|
||||
.syncExternalProviders(const <ExternalCodeAgentAcpSyncedProvider>[
|
||||
ExternalCodeAgentAcpSyncedProvider(
|
||||
providerId: 'codex',
|
||||
label: 'Codex',
|
||||
endpoint: 'https://acp-server.svc.plus/codex/acp/rpc',
|
||||
authorizationHeader: '',
|
||||
enabled: true,
|
||||
),
|
||||
]);
|
||||
|
||||
expect(bridge.methods, <String>['xworkmate.providers.sync']);
|
||||
},
|
||||
);
|
||||
expect(bridge.methods, isEmpty);
|
||||
});
|
||||
|
||||
test(
|
||||
'uses bridge routing resolve for preflight provider selection',
|
||||
|
||||
@ -79,7 +79,7 @@ void main() {
|
||||
lastSyncAt: 123456789,
|
||||
remoteServerSummary:
|
||||
const AcpBridgeServerRemoteServerSummary(
|
||||
endpoint: 'wss://gateway.svc.plus',
|
||||
endpoint: 'https://xworkmate-bridge.svc.plus',
|
||||
hasAdvancedOverrides: false,
|
||||
),
|
||||
),
|
||||
|
||||
@ -58,6 +58,15 @@ void main() {
|
||||
expect(controller.accountSyncState?.profileScope, 'tenant-shared');
|
||||
expect(controller.accountSyncState?.tokenConfigured.apisix, isTrue);
|
||||
expect(await store.loadAccountSessionToken(), 'session-token');
|
||||
expect(
|
||||
controller
|
||||
.snapshot
|
||||
.acpBridgeServerModeConfig
|
||||
.cloudSynced
|
||||
.remoteServerSummary
|
||||
.endpoint,
|
||||
'https://xworkmate-bridge.svc.plus',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user