Refine bridge routing and settings cleanup

This commit is contained in:
Haitao Pan 2026-04-22 00:49:41 +08:00
parent a05d84bbeb
commit 19f1ce306f
10 changed files with 266 additions and 374 deletions

View File

@ -3,17 +3,17 @@
Last Updated: 2026-04-19
本文件记录当前 `Settings -> Integrations` 在主链中的职责边界,以及
`acpBridgeServerModeConfig`有效配置仲裁规则。
`acpBridgeServerModeConfig` 在 settings surface 中的配置仲裁规则。
## Current Rule
- Settings 只管理 Bridge 连接参数、account sync 元数据和本地编辑态
- `AcpBridgeServerModeConfig.effective` 是运行时实际生效配置
- `AcpBridgeServerModeConfig.effective` 只用于 settings surface 的连接与展示语义
- `selfHosted` 优先级高于 `cloudSynced`
- `cloudSynced` 只在 manual Bridge 未配置时作为有效回退来源
- `cloudSynced` 只在 manual Bridge 未配置时作为 settings metadata 回退来源
- app 不从本地 endpoint preset、旧 module 配置、历史 fallback 恢复 provider catalog
- `xworkmate-bridge` 仍然是 provider catalog、gateway capability、routing resolve 的唯一真源
- `BRIDGE_SERVER_URL` 只属于 `AccountSyncState` 元数据
- `BRIDGE_SERVER_URL` 只属于 `AccountSyncState` 元数据,不参与 assistant runtime endpoint 选择
- `BRIDGE_AUTH_TOKEN` 只进入 secure storage / managed secret
## Canonical State Model
@ -119,18 +119,18 @@ stateDiagram-v2
DefaultEffective --> CloudEffective: cloud sync 恢复
note right of BridgeEffective
source = bridge
effective.endpoint = selfHosted.serverUrl
source = bridge metadata
used by settings only
end note
note right of CloudEffective
source = cloud
effective.endpoint = accountSyncState.syncedDefaults.bridgeServerUrl
source = cloud metadata
used by settings only
end note
note right of DefaultEffective
source = default
effective.endpoint = kManagedBridgeServerUrl
source = managed bridge origin
assistant runtime fixed to kManagedBridgeServerUrl
end note
```

View File

@ -636,27 +636,8 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
}
Uri? resolveBridgeAcpEndpointInternal() {
final modeConfig = settings.acpBridgeServerModeConfig;
final cloudEndpoint = _activeCloudSyncedBridgeEndpointInternal();
if (cloudEndpoint.isNotEmpty) {
final uri = Uri.tryParse(cloudEndpoint);
if (uri != null) return uri.replace(query: null, fragment: null);
}
if (modeConfig.usesSelfHostedBase) {
final candidate = modeConfig.selfHosted.serverUrl.trim();
if (candidate.isNotEmpty) {
final uri = Uri.tryParse(candidate);
final scheme = uri?.scheme.trim().toLowerCase() ?? '';
if (uri != null &&
kSupportedExternalAcpEndpointSchemes.contains(scheme)) {
return uri.replace(query: null, fragment: null);
}
}
}
return null;
final uri = Uri.parse(kManagedBridgeServerUrl);
return uri.replace(query: null, fragment: null);
}
Uri? resolveExternalAcpEndpointForTargetInternal(AssistantExecutionTarget _) {
@ -664,11 +645,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
}
bool isBridgeAcpRuntimeConfiguredInternal() {
final modeConfig = settings.acpBridgeServerModeConfig;
if (modeConfig.usesSelfHostedBase) {
return modeConfig.selfHosted.isConfigured;
}
return _activeCloudSyncedBridgeEndpointInternal().isNotEmpty;
return true;
}
Uri? resolveExternalAcpEndpointForRequestInternal(
@ -677,22 +654,6 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
return resolveBridgeAcpEndpointInternal();
}
String _activeCloudSyncedBridgeEndpointInternal() {
final syncState = settingsControllerInternal.accountSyncState;
final syncedEndpoint =
syncState?.syncedDefaults.bridgeServerUrl.trim() ?? '';
if (syncState?.syncState.trim().toLowerCase() == 'ready' &&
syncState?.tokenConfigured.bridge == true &&
syncedEndpoint.isNotEmpty) {
return isSupportedExternalAcpEndpoint(syncedEndpoint)
? syncedEndpoint
: '';
}
return '';
}
Uri? gatewayProfileBaseUriInternal(GatewayConnectionProfile profile) {
final host = profile.host.trim();
if (host.isEmpty || profile.port <= 0) {
@ -722,16 +683,6 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
return envToken;
}
final modeConfig = settings.acpBridgeServerModeConfig;
if (modeConfig.usesSelfHostedBase) {
final manualToken = await settingsControllerInternal
.loadSecretValueByRef(modeConfig.selfHosted.passwordRef);
if (manualToken.trim().isNotEmpty) {
return manualToken.trim();
}
return null;
}
final bridgeToken = (await storeInternal.loadAccountManagedSecret(
target: kAccountManagedSecretTargetBridgeAuthToken,
))?.trim();

View File

@ -16,6 +16,78 @@ import '../../widgets/surface_card.dart';
import 'settings_account_panel.dart';
import 'settings_about_panel.dart';
Future<Map<String, dynamic>> loadBridgeMetadataForSettingsAbout({
required Uri bridgeEndpoint,
required Future<String?> Function(Uri endpoint) authorizationResolver,
HttpClient Function()? clientFactory,
}) async {
final pingEndpoint = bridgeEndpoint.replace(
path: '/api/ping',
query: null,
fragment: null,
);
final authorizationHeader = await authorizationResolver(pingEndpoint);
if (authorizationHeader == null || authorizationHeader.trim().isEmpty) {
return const <String, dynamic>{
'status': 'unavailable',
'version': '',
'commit': '',
'image': '',
'buildDate': '',
};
}
final client = (clientFactory ?? HttpClient.new)()
..connectionTimeout = const Duration(seconds: 4);
try {
final request = await client
.getUrl(pingEndpoint)
.timeout(const Duration(seconds: 4));
request.headers.set(
HttpHeaders.authorizationHeader,
'Bearer $authorizationHeader',
);
request.headers.set(HttpHeaders.acceptHeader, 'application/json');
final response = await request.close().timeout(const Duration(seconds: 4));
final body = await utf8
.decodeStream(response)
.timeout(const Duration(seconds: 4));
if (response.statusCode < 200 || response.statusCode >= 300) {
return const <String, dynamic>{
'status': 'unavailable',
'version': '',
'commit': '',
'image': '',
'buildDate': '',
};
}
final decoded = jsonDecode(body);
if (decoded is Map<String, dynamic>) {
return decoded;
}
if (decoded is Map) {
return decoded.cast<String, dynamic>();
}
} catch (_) {
return const <String, dynamic>{
'status': 'unavailable',
'version': '',
'commit': '',
'image': '',
'buildDate': '',
};
} finally {
client.close(force: true);
}
return const <String, dynamic>{
'status': 'unavailable',
'version': '',
'commit': '',
'image': '',
'buildDate': '',
};
}
class SettingsPage extends StatefulWidget {
const SettingsPage({
super.key,
@ -54,7 +126,8 @@ class _SettingsPageState extends State<SettingsPage> {
final settings = widget.controller.settings;
_lastSavedAccountBaseUrl = settings.accountBaseUrl;
_lastSavedAccountIdentifier = settings.accountUsername;
_lastSavedBridgeUrl = settings.acpBridgeServerModeConfig.selfHosted.serverUrl;
_lastSavedBridgeUrl =
settings.acpBridgeServerModeConfig.selfHosted.serverUrl;
_accountBaseUrlController = TextEditingController(
text: _lastSavedAccountBaseUrl,
);
@ -83,7 +156,14 @@ class _SettingsPageState extends State<SettingsPage> {
Future<void> _loadBridgeToken() async {
final token = await widget.controller.settingsController
.loadSecretValueByRef(widget.controller.settings.acpBridgeServerModeConfig.selfHosted.passwordRef);
.loadSecretValueByRef(
widget
.controller
.settings
.acpBridgeServerModeConfig
.selfHosted
.passwordRef,
);
if (mounted) {
_bridgeTokenController.text = token;
}
@ -109,7 +189,7 @@ class _SettingsPageState extends State<SettingsPage> {
_lastSavedBridgeUrl = bridgeConfig.selfHosted.serverUrl;
}
Future<void> _saveAccountProfile(
Future<void> _persistAccountProfileSettings(
SettingsSnapshot settings, {
required bool isManualBridge,
}) async {
@ -121,11 +201,8 @@ class _SettingsPageState extends State<SettingsPage> {
),
);
// Resolve the effective config based on the new sources
final nextEffective = widget.controller.settingsController.resolveAcpBridgeServerEffectiveConfig(
config: nextBridgeConfig,
accountSyncState: widget.controller.settingsController.accountSyncState,
);
final nextEffective = widget.controller.settingsController
.resolveAcpBridgeServerEffectiveConfig(config: nextBridgeConfig);
final nextSettings = settings.copyWith(
accountBaseUrl: _accountBaseUrlController.text.trim(),
@ -146,14 +223,15 @@ class _SettingsPageState extends State<SettingsPage> {
_lastSavedAccountBaseUrl = nextSettings.accountBaseUrl;
_lastSavedAccountIdentifier = nextSettings.accountUsername;
_lastSavedBridgeUrl = nextSettings.acpBridgeServerModeConfig.selfHosted.serverUrl;
_lastSavedBridgeUrl =
nextSettings.acpBridgeServerModeConfig.selfHosted.serverUrl;
}
Future<void> _loginAccount(SettingsSnapshot settings) async {
final baseUrl = _accountBaseUrlController.text.trim();
final identifier = _accountIdentifierController.text.trim();
try {
await _saveAccountProfile(settings, isManualBridge: false);
await _persistAccountProfileSettings(settings, isManualBridge: false);
await widget.controller.settingsController.loginAccount(
baseUrl: baseUrl,
identifier: identifier,
@ -166,7 +244,7 @@ class _SettingsPageState extends State<SettingsPage> {
}
Future<void> _syncAccount(SettingsSnapshot settings) async {
await _saveAccountProfile(settings, isManualBridge: false);
await _persistAccountProfileSettings(settings, isManualBridge: false);
await widget.controller.settingsController.syncAccountSettings(
baseUrl: _accountBaseUrlController.text.trim(),
);
@ -176,7 +254,7 @@ class _SettingsPageState extends State<SettingsPage> {
Future<void> _verifyAccountMfa(SettingsSnapshot settings) async {
try {
await _saveAccountProfile(settings, isManualBridge: false);
await _persistAccountProfileSettings(settings, isManualBridge: false);
await widget.controller.settingsController.verifyAccountMfa(
baseUrl: _accountBaseUrlController.text.trim(),
code: _accountMfaCodeController.text.trim(),
@ -251,50 +329,11 @@ class _SettingsPageState extends State<SettingsPage> {
}
Future<Map<String, dynamic>> _loadBridgeMetadata() async {
final client = HttpClient()..connectionTimeout = const Duration(seconds: 4);
try {
final request = await client
.getUrl(Uri.parse('$kManagedBridgeServerUrl/api/ping'))
.timeout(const Duration(seconds: 4));
request.headers.set(HttpHeaders.acceptHeader, 'application/json');
final response = await request.close().timeout(const Duration(seconds: 4));
final body = await utf8
.decodeStream(response)
.timeout(const Duration(seconds: 4));
if (response.statusCode < 200 || response.statusCode >= 300) {
return <String, dynamic>{
'status': 'error',
'version': '',
'commit': '',
'image': '',
'buildDate': '',
};
}
final decoded = jsonDecode(body);
if (decoded is Map<String, dynamic>) {
return decoded;
}
if (decoded is Map) {
return decoded.cast<String, dynamic>();
}
} catch (_) {
return const <String, dynamic>{
'status': 'unavailable',
'version': '',
'commit': '',
'image': '',
'buildDate': '',
};
} finally {
client.close(force: true);
}
return const <String, dynamic>{
'status': 'unavailable',
'version': '',
'commit': '',
'image': '',
'buildDate': '',
};
return loadBridgeMetadataForSettingsAbout(
bridgeEndpoint: Uri.parse(kManagedBridgeServerUrl),
authorizationResolver:
widget.controller.resolveGatewayAcpAuthorizationHeaderInternal,
);
}
@override
@ -357,12 +396,13 @@ class _SettingsPageState extends State<SettingsPage> {
bridgeUrlController: _bridgeUrlController,
bridgeTokenController: _bridgeTokenController,
onSaveAccountProfile: ({required bool isManualBridge}) =>
_saveAccountProfile(
_persistAccountProfileSettings(
widget.controller.settings,
isManualBridge: isManualBridge,
),
onLogin: () => _loginAccount(widget.controller.settings),
onVerifyMfa: () => _verifyAccountMfa(widget.controller.settings),
onVerifyMfa: () =>
_verifyAccountMfa(widget.controller.settings),
onCancelMfa: _cancelAccountMfa,
onSync: () => _syncAccount(widget.controller.settings),
onLogout: _logoutAccount,

View File

@ -538,7 +538,7 @@ class SettingsController extends ChangeNotifier {
);
}
} catch (_) {
// Best effort only. Directory watch below remains as a fallback.
// Best effort only. If file watching fails, directory watching may still work.
}
}
if (directory != null) {

View File

@ -51,8 +51,7 @@ extension SettingsControllerAccountExtension on SettingsController {
Future<String> loadEffectiveGatewayToken({int? profileIndex}) async {
final resolvedProfileIndex = (profileIndex ?? kGatewayRemoteProfileIndex)
.clamp(0, kGatewayProfileListLength - 1);
// Use the Single Source of Truth from the effective configuration for the primary profile
if (resolvedProfileIndex == kGatewayRemoteProfileIndex) {
final effective = snapshotInternal.acpBridgeServerModeConfig.effective;
if (effective.tokenRef.isNotEmpty) {
@ -63,7 +62,6 @@ extension SettingsControllerAccountExtension on SettingsController {
}
}
// Local Override / Vault / Cloud Sync (Fallback if effective token is missing or for other profiles)
return resolveSecretValueInternal(
refName: gatewayTokenRefForProfileInternal(resolvedProfileIndex),
fallbackRefName: SecretStore.gatewayTokenRefKey(resolvedProfileIndex),
@ -77,10 +75,6 @@ extension SettingsControllerAccountExtension on SettingsController {
final resolvedProfileIndex = (profileIndex ?? kGatewayRemoteProfileIndex)
.clamp(0, kGatewayProfileListLength - 1);
// Manual bridge usually uses a single token/key, but we check if the effective configuration
// points to bridge and if a password override is actually needed.
// For now, we fall back to the standard resolution logic.
return resolveSecretValueInternal(
refName: gatewayPasswordRefForProfileInternal(resolvedProfileIndex),
fallbackRefName: SecretStore.gatewayPasswordRefKey(resolvedProfileIndex),
@ -120,12 +114,7 @@ extension SettingsControllerAccountExtension on SettingsController {
AcpBridgeServerEffectiveConfig resolveAcpBridgeServerEffectiveConfig({
required AcpBridgeServerModeConfig config,
AccountSyncState? accountSyncState,
}) => resolveAcpBridgeServerEffectiveConfigInternal(
this,
config: config,
accountSyncState: accountSyncState,
);
}) => resolveAcpBridgeServerEffectiveConfigInternal(this, config: config);
List<SecretReferenceEntry> buildSecretReferences() {
final entries = <SecretReferenceEntry>[

View File

@ -284,18 +284,7 @@ Future<AccountSyncResult> syncAccountSettingsInternal(
target: kAccountManagedSecretTargetBridgeAuthToken,
value: bridgeToken,
);
final syncedBridgeServerUrl = _resolveBridgeServerUrl(syncPayload);
if (!isSupportedExternalAcpEndpoint(syncedBridgeServerUrl)) {
return _persistAccountSyncContractFailureInternal(
controller,
message: 'Bridge endpoint is unavailable',
quiet: quiet,
);
}
final resolvedBridgeServerUrl = _resolveCurrentBridgeServerUrl(
controller,
bridgeServerUrlOverride: syncedBridgeServerUrl,
);
final syncedBridgeServerUrl = _extractBridgeServerUrlMetadata(syncPayload);
await controller.storeInternal.clearAccountManagedSecret(
target: kAccountManagedSecretTargetAIGatewayAccessToken,
);
@ -305,12 +294,12 @@ Future<AccountSyncResult> syncAccountSettingsInternal(
final nextState = AccountSyncState.defaults().copyWith(
syncedDefaults: AccountRemoteProfile.defaults().copyWith(
bridgeServerUrl: resolvedBridgeServerUrl,
bridgeServerUrl: syncedBridgeServerUrl,
),
syncState: 'ready',
syncMessage: 'Bridge access synced',
lastSyncAtMs: DateTime.now().millisecondsSinceEpoch,
lastSyncSource: resolvedBridgeServerUrl,
lastSyncSource: syncedBridgeServerUrl,
lastSyncError: '',
profileScope: 'bridge',
tokenConfigured: const AccountTokenConfigured(
@ -326,7 +315,6 @@ Future<AccountSyncResult> syncAccountSettingsInternal(
final nextEffective = resolveAcpBridgeServerEffectiveConfigInternal(
controller,
config: currentModeConfig,
accountSyncState: nextState,
);
final identifier =
@ -347,7 +335,7 @@ Future<AccountSyncResult> syncAccountSettingsInternal(
lastSyncAt: nextState.lastSyncAtMs,
remoteServerSummary: currentModeConfig.cloudSynced.remoteServerSummary
.copyWith(
endpoint: nextEffective.endpoint,
endpoint: syncedBridgeServerUrl,
hasAdvancedOverrides: false,
),
),
@ -578,7 +566,7 @@ Future<AccountSyncResult> _persistAccountSyncContractFailureInternal(
);
}
String _resolveBridgeServerUrl(Map<String, dynamic> payload) {
String _extractBridgeServerUrlMetadata(Map<String, dynamic> payload) {
final explicit = _stringValue(payload['BRIDGE_SERVER_URL']);
if (explicit.isNotEmpty) {
return explicit;
@ -593,10 +581,7 @@ String _resolveBridgeServerUrl(Map<String, dynamic> payload) {
AcpBridgeServerEffectiveConfig resolveAcpBridgeServerEffectiveConfigInternal(
SettingsController controller, {
required AcpBridgeServerModeConfig config,
AccountSyncState? accountSyncState,
}) {
// Priority 1: Manual Bridge (Self-Hosted)
// Logic: Must have a valid URL and be explicitly intended (we assume if it's configured, it's intended)
if (config.selfHosted.isConfigured) {
return AcpBridgeServerEffectiveConfig(
endpoint: config.selfHosted.serverUrl,
@ -606,20 +591,6 @@ AcpBridgeServerEffectiveConfig resolveAcpBridgeServerEffectiveConfigInternal(
);
}
// Priority 2: Cloud Sync (svc.plus)
// Logic: Check the synced state for a valid endpoint and token
final syncedUrl =
accountSyncState?.syncedDefaults.bridgeServerUrl.trim() ?? '';
final hasSyncedToken = accountSyncState?.tokenConfigured.bridge == true;
if (isSupportedExternalAcpEndpoint(syncedUrl) && hasSyncedToken) {
return AcpBridgeServerEffectiveConfig(
endpoint: syncedUrl,
tokenRef: kAccountManagedSecretTargetBridgeAuthToken,
source: 'cloud',
reason: 'Synced cloud configuration from svc.plus is active',
);
}
return AcpBridgeServerEffectiveConfig(
endpoint: '',
tokenRef: '',
@ -628,21 +599,6 @@ AcpBridgeServerEffectiveConfig resolveAcpBridgeServerEffectiveConfigInternal(
);
}
String _resolveCurrentBridgeServerUrl(
SettingsController controller, {
String bridgeServerUrlOverride = '',
}) {
final override = bridgeServerUrlOverride.trim();
if (override.isNotEmpty) {
return override;
}
return controller
.snapshotInternal
.acpBridgeServerModeConfig
.effective
.endpoint;
}
int _parseExpiresAtMs(Object? value) {
if (value is int) {
return value;

View File

@ -0,0 +1,106 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:xworkmate/features/settings/settings_page_core.dart';
void main() {
group('settings about bridge metadata', () {
test('loads bridge metadata with bearer authorization', () async {
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
addTearDown(() async {
await server.close(force: true);
});
late Map<String, String> requestHeaders;
server.listen((request) async {
requestHeaders = {
'authorization':
request.headers.value(HttpHeaders.authorizationHeader) ?? '',
'accept': request.headers.value(HttpHeaders.acceptHeader) ?? '',
};
request.response
..statusCode = HttpStatus.ok
..headers.contentType = ContentType.json
..write(
jsonEncode(<String, dynamic>{
'status': 'ok',
'version': '991ecb0',
'commit': '991ecb0',
'image': 'ghcr.io/x-evor/xworkmate-bridge:991ecb0',
'buildDate': '2026-04-21',
}),
);
await request.response.close();
});
final metadata = await loadBridgeMetadataForSettingsAbout(
bridgeEndpoint: Uri.parse(
'http://${server.address.address}:${server.port}',
),
authorizationResolver: (_) async => 'bridge-token',
);
expect(requestHeaders['authorization'], 'Bearer bridge-token');
expect(requestHeaders['accept'], 'application/json');
expect(metadata['status'], 'ok');
expect(metadata['version'], '991ecb0');
expect(metadata['commit'], '991ecb0');
expect(metadata['image'], 'ghcr.io/x-evor/xworkmate-bridge:991ecb0');
expect(metadata['buildDate'], '2026-04-21');
});
test('returns unavailable when bridge authorization is missing', () async {
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
addTearDown(() async {
await server.close(force: true);
});
var receivedRequest = false;
server.listen((request) async {
receivedRequest = true;
request.response.statusCode = HttpStatus.ok;
await request.response.close();
});
final metadata = await loadBridgeMetadataForSettingsAbout(
bridgeEndpoint: Uri.parse(
'http://${server.address.address}:${server.port}',
),
authorizationResolver: (_) async => null,
);
expect(receivedRequest, isFalse);
expect(metadata['status'], 'unavailable');
expect(metadata['version'], '');
expect(metadata['commit'], '');
expect(metadata['image'], '');
expect(metadata['buildDate'], '');
});
test('returns unavailable when authorized bridge ping fails', () async {
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
addTearDown(() async {
await server.close(force: true);
});
server.listen((request) async {
request.response.statusCode = HttpStatus.unauthorized;
await request.response.close();
});
final metadata = await loadBridgeMetadataForSettingsAbout(
bridgeEndpoint: Uri.parse(
'http://${server.address.address}:${server.port}',
),
authorizationResolver: (_) async => 'bridge-token',
);
expect(metadata['status'], 'unavailable');
expect(metadata['version'], '');
expect(metadata['commit'], '');
expect(metadata['image'], '');
expect(metadata['buildDate'], '');
});
});
}

View File

@ -9,7 +9,7 @@ import 'package:xworkmate/runtime/secure_config_store.dart';
void main() {
group('Bridge runtime cleanup', () {
test(
'uses synced bridge endpoint only when account sync has a bridge token',
'keeps the managed bridge endpoint fixed even when account sync carries a bridge URL',
() async {
final storeRoot = await Directory.systemTemp.createTemp(
'xworkmate-bridge-runtime-cleanup-',
@ -61,7 +61,7 @@ void main() {
expect(
controller.resolveBridgeAcpEndpointInternal()?.toString(),
'https://xworkmate-bridge-alt.svc.plus',
kManagedBridgeServerUrl,
);
expect(
controller
@ -69,7 +69,7 @@ void main() {
AssistantExecutionTarget.gateway,
)
?.toString(),
'https://xworkmate-bridge-alt.svc.plus',
kManagedBridgeServerUrl,
);
expect(await store.loadAccountSyncState(), isNotNull);
expect(
@ -80,7 +80,7 @@ void main() {
);
test(
'does not fallback to the managed bridge endpoint when signed out',
'keeps the managed bridge endpoint fixed when signed out',
() {
final controller = AppController(
environmentOverride: const <String, String>{
@ -89,7 +89,10 @@ void main() {
);
addTearDown(controller.dispose);
expect(controller.resolveBridgeAcpEndpointInternal(), isNull);
expect(
controller.resolveBridgeAcpEndpointInternal()?.toString(),
kManagedBridgeServerUrl,
);
},
);
@ -139,10 +142,9 @@ void main() {
addTearDown(controller.dispose);
await controller.settingsControllerInternal.initialize();
final bridgeHeader = await controller
.resolveGatewayAcpAuthorizationHeaderInternal(
Uri.parse('https://xworkmate-bridge.svc.plus/acp/rpc'),
);
final bridgeHeader = await controller.resolveGatewayAcpAuthorizationHeaderInternal(
Uri.parse('$kManagedBridgeServerUrl/acp/rpc'),
);
final unrelatedHeader = await controller
.resolveGatewayAcpAuthorizationHeaderInternal(
Uri.parse('https://unrelated.example.com/acp/rpc'),
@ -153,43 +155,6 @@ void main() {
},
);
test(
'ignores legacy INTERNAL_SERVICE_TOKEN for managed bridge auth resolution',
() async {
final storeRoot = await Directory.systemTemp.createTemp(
'xworkmate-bridge-auth-resolver-legacy-',
);
addTearDown(() async {
if (await storeRoot.exists()) {
await storeRoot.delete(recursive: true);
}
});
final store = SecureConfigStore(
secretRootPathResolver: () async => '${storeRoot.path}/secrets',
appDataRootPathResolver: () async => '${storeRoot.path}/app-data',
supportRootPathResolver: () async => '${storeRoot.path}/support',
enableSecureStorage: false,
);
await store.initialize();
final controller = AppController(
store: store,
environmentOverride: const <String, String>{
'INTERNAL_SERVICE_TOKEN': 'legacy-bridge-token',
},
);
addTearDown(controller.dispose);
final bridgeHeader = await controller
.resolveGatewayAcpAuthorizationHeaderInternal(
Uri.parse('https://xworkmate-bridge.svc.plus/acp/rpc'),
);
expect(bridgeHeader, isNull);
},
);
test(
'runtime coordinator only exposes remote and offline gateway modes',
() {

View File

@ -152,7 +152,7 @@ void main() {
);
test(
'desktop bridge auth resolver sends managed bridge bearer for capabilities HTTP',
'desktop bridge auth resolver sends bearer when the caller asks for managed bridge auth',
() async {
final capture = await _startAcpHttpServer();
addTearDown(capture.close);
@ -182,26 +182,14 @@ void main() {
target: kAccountManagedSecretTargetBridgeAuthToken,
value: 'bridge-token',
);
await store.saveAccountSyncState(
AccountSyncState.defaults().copyWith(
syncedDefaults: AccountRemoteProfile.defaults().copyWith(
bridgeServerUrl: capture.baseEndpoint.toString(),
),
syncState: 'ready',
tokenConfigured: const AccountTokenConfigured(
bridge: true,
vault: false,
apisix: false,
),
),
final client = GatewayAcpClient(
endpointResolver: () => capture.baseEndpoint,
authorizationResolver: (_) async => 'bridge-token',
);
final controller = AppController(store: store);
addTearDown(controller.dispose);
await controller.settingsControllerInternal.initialize();
await controller.gatewayAcpClientInternal.loadCapabilities(
forceRefresh: true,
await client.request(
method: 'acp.capabilities',
params: const <String, dynamic>{},
);
expect(capture.authorizationHeader, 'Bearer bridge-token');
@ -258,80 +246,20 @@ void main() {
},
);
test(
'desktop bridge auth resolver resolves manual bridge token when configured',
() async {
final storeRoot = await Directory.systemTemp.createTemp(
'xworkmate-acp-auth-bridge-manual-',
);
addTearDown(() async {
if (await storeRoot.exists()) {
try {
await storeRoot.delete(recursive: true);
} on FileSystemException {
// Temp cleanup is best effort here.
}
}
});
final store = SecureConfigStore(
secretRootPathResolver: () async => '${storeRoot.path}/secrets',
appDataRootPathResolver: () async => '${storeRoot.path}/app-data',
supportRootPathResolver: () async => '${storeRoot.path}/support',
enableSecureStorage: false,
);
await store.initialize();
final settings = SettingsSnapshot.defaults().copyWith(
acpBridgeServerModeConfig: AcpBridgeServerModeConfig.defaults()
.copyWith(
effective: const AcpBridgeServerEffectiveConfig(
endpoint: 'https://manual-bridge.example.com',
tokenRef: 'acp_bridge_server_password',
source: 'bridge',
reason: 'Manual test configuration',
),
selfHosted: AcpBridgeServerSelfHostedConfig.defaults().copyWith(
serverUrl: 'https://manual-bridge.example.com',
username: 'admin',
),
),
);
await store.saveSettingsSnapshot(settings);
await store.saveSecretValueByRef(
settings.acpBridgeServerModeConfig.selfHosted.passwordRef,
'manual-token',
);
final controller = AppController(store: store);
addTearDown(controller.dispose);
await controller.settingsControllerInternal.initialize();
final header = await controller
.resolveGatewayAcpAuthorizationHeaderInternal(
Uri.parse('https://manual-bridge.example.com/acp/rpc'),
);
expect(header, 'manual-token');
},
);
test(
'desktop task execution routes Hermes through bridge RPC with provider params',
() async {
final capture = await _startAcpHttpServer();
addTearDown(capture.close);
final controller = await _syncedControllerForBridgeEndpoint(
capture.baseEndpoint,
final client = GatewayAcpClient(
endpointResolver: () => capture.baseEndpoint,
authorizationResolver: (_) async => 'bridge-token',
);
addTearDown(controller.dispose);
final transport = ExternalCodeAgentAcpDesktopTransport(
client: controller.gatewayAcpClientInternal,
endpointResolver:
controller.resolveExternalAcpEndpointForTargetInternal,
taskEndpointResolver:
controller.resolveExternalAcpEndpointForRequestInternal,
client: client,
endpointResolver: (_) => capture.baseEndpoint,
taskEndpointResolver: (_) => capture.baseEndpoint,
);
await transport.executeTask(
@ -359,17 +287,15 @@ void main() {
() async {
final capture = await _startAcpHttpServer();
addTearDown(capture.close);
final controller = await _syncedControllerForBridgeEndpoint(
capture.baseEndpoint,
final client = GatewayAcpClient(
endpointResolver: () => capture.baseEndpoint,
authorizationResolver: (_) async => 'bridge-token',
);
addTearDown(controller.dispose);
final transport = ExternalCodeAgentAcpDesktopTransport(
client: controller.gatewayAcpClientInternal,
endpointResolver:
controller.resolveExternalAcpEndpointForTargetInternal,
taskEndpointResolver:
controller.resolveExternalAcpEndpointForRequestInternal,
client: client,
endpointResolver: (_) => capture.baseEndpoint,
taskEndpointResolver: (_) => capture.baseEndpoint,
);
await transport.executeTask(
@ -416,48 +342,6 @@ GoTaskServiceRequest _taskRequest({
);
}
Future<AppController> _syncedControllerForBridgeEndpoint(Uri endpoint) async {
final storeRoot = await Directory.systemTemp.createTemp(
'xworkmate-acp-auth-provider-endpoint-',
);
addTearDown(() async {
if (await storeRoot.exists()) {
try {
await storeRoot.delete(recursive: true);
} on FileSystemException {
// Temp cleanup is best effort here.
}
}
});
final store = SecureConfigStore(
secretRootPathResolver: () async => '${storeRoot.path}/secrets',
appDataRootPathResolver: () async => '${storeRoot.path}/app-data',
supportRootPathResolver: () async => '${storeRoot.path}/support',
enableSecureStorage: false,
);
await store.initialize();
await store.saveAccountSyncState(
AccountSyncState.defaults().copyWith(
syncedDefaults: AccountRemoteProfile.defaults().copyWith(
bridgeServerUrl: endpoint.toString(),
),
syncState: 'ready',
tokenConfigured: const AccountTokenConfigured(
bridge: true,
vault: false,
apisix: false,
),
),
);
await store.saveAccountManagedSecret(
target: kAccountManagedSecretTargetBridgeAuthToken,
value: 'bridge-token',
);
final controller = AppController(store: store);
await controller.settingsControllerInternal.initialize();
return controller;
}
Future<_CapturedAcpHttpServer> _startAcpHttpServer() async {
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
final capture = _CapturedAcpHttpServer._(

View File

@ -289,7 +289,7 @@ void main() {
);
test(
'syncAccountSettings refreshes managed bridge contract from protected account profile',
'syncAccountSettings refreshes managed bridge metadata from protected account profile',
() async {
final storeRoot = await Directory.systemTemp.createTemp(
'xworkmate-account-managed-bridge-refresh-',
@ -365,7 +365,7 @@ void main() {
);
test(
'synced bridge url becomes runtime endpoint only with a configured bridge token',
'managed bridge endpoint stays fixed regardless of synced bridge url metadata',
() async {
final storeRoot = await Directory.systemTemp.createTemp(
'xworkmate-account-managed-bridge-runtime-',
@ -412,28 +412,28 @@ void main() {
expect(
controller.resolveGatewayAcpEndpointInternal()?.toString(),
'https://xworkmate-bridge-alt.svc.plus',
kManagedBridgeServerUrl,
);
expect(
await controller.resolveGatewayAcpAuthorizationHeaderInternal(
Uri.parse('https://xworkmate-bridge-alt.svc.plus/acp/rpc'),
),
'bridge-token',
isNull,
);
expect(
await controller.resolveGatewayAcpAuthorizationHeaderInternal(
Uri.parse('$kManagedBridgeServerUrl/acp/rpc'),
),
isNull,
'bridge-token',
);
},
);
test(
'syncAccountSettings blocks and clears stale token when bridge endpoint is unavailable',
'syncAccountSettings succeeds when bridge url metadata is missing',
() async {
final storeRoot = await Directory.systemTemp.createTemp(
'xworkmate-account-managed-bridge-missing-url-',
'xworkmate-account-managed-bridge-missing-metadata-',
);
addTearDown(() async {
if (await storeRoot.exists()) {
@ -483,14 +483,15 @@ void main() {
baseUrl: 'https://accounts.svc.plus',
);
expect(result.state, 'blocked');
expect(result.message, 'Bridge endpoint is unavailable');
expect(result.state, 'ready');
expect(result.message, 'Bridge access synced');
expect(
await store.loadAccountManagedSecret(
target: kAccountManagedSecretTargetBridgeAuthToken,
),
isNull,
'fresh-bridge-token',
);
expect(controller.accountSyncState!.syncedDefaults.bridgeServerUrl, '');
},
);