fix: remove stale ACP gateway fallback routing
This commit is contained in:
parent
2f81f53f9d
commit
8e07f87b70
@ -50,27 +50,11 @@ extension AppControllerDesktopExternalAcpRouting on AppController {
|
||||
if (providerId.isEmpty || endpoint.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
var authorizationHeader = effectiveProfile.authRef.trim().isEmpty
|
||||
final authorizationHeader = effectiveProfile.authRef.trim().isEmpty
|
||||
? ''
|
||||
: await settingsControllerInternal.resolveSecretValueInternal(
|
||||
refName: effectiveProfile.authRef.trim(),
|
||||
);
|
||||
if (authorizationHeader.isEmpty &&
|
||||
builtinProvider != null &&
|
||||
settings.acpBridgeServerModeConfig.usesSelfHostedBase) {
|
||||
final selfHosted = settings.acpBridgeServerModeConfig.selfHosted;
|
||||
final username = selfHosted.username.trim();
|
||||
final passwordRef = selfHosted.passwordRef.trim();
|
||||
final password = passwordRef.isEmpty
|
||||
? ''
|
||||
: await settingsControllerInternal.loadSecretValueByRef(
|
||||
passwordRef,
|
||||
);
|
||||
if (username.isNotEmpty && password.trim().isNotEmpty) {
|
||||
authorizationHeader =
|
||||
'Basic ${base64Encode(utf8.encode('$username:${password.trim()}'))}';
|
||||
}
|
||||
}
|
||||
providers.add(
|
||||
ExternalCodeAgentAcpSyncedProvider(
|
||||
providerId: providerId,
|
||||
|
||||
@ -53,8 +53,28 @@ Future<void> refreshAcpCapabilitiesRuntimeInternal(
|
||||
bool persistMountTargets = false,
|
||||
}) async {
|
||||
try {
|
||||
final target = controller.assistantExecutionTargetForSession(
|
||||
controller.sessionsControllerInternal.currentSessionKey,
|
||||
);
|
||||
final resolvedProvider =
|
||||
target == AssistantExecutionTarget.singleAgent
|
||||
? (controller.singleAgentResolvedProviderForSession(
|
||||
controller.sessionsControllerInternal.currentSessionKey,
|
||||
) ??
|
||||
controller.currentSingleAgentResolvedProvider)
|
||||
: null;
|
||||
final endpointOverride = resolvedProvider == null
|
||||
? null
|
||||
: controller.resolveSingleAgentEndpointInternal(resolvedProvider);
|
||||
final authorizationOverride = resolvedProvider == null
|
||||
? ''
|
||||
: await controller.resolveSingleAgentAuthorizationHeaderForProviderInternal(
|
||||
resolvedProvider,
|
||||
);
|
||||
await controller.gatewayAcpClientInternal.loadCapabilities(
|
||||
forceRefresh: forceRefresh,
|
||||
endpointOverride: endpointOverride,
|
||||
authorizationOverride: authorizationOverride,
|
||||
);
|
||||
} catch (_) {
|
||||
// Keep mount refresh resilient when ACP is temporarily unavailable.
|
||||
|
||||
@ -702,18 +702,22 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
return '';
|
||||
}
|
||||
|
||||
Future<String> resolveSingleAgentAuthorizationHeaderForProviderInternal(
|
||||
SingleAgentProvider provider,
|
||||
) async {
|
||||
final endpoint = resolveSingleAgentEndpointInternal(provider);
|
||||
if (endpoint == null) {
|
||||
return '';
|
||||
}
|
||||
return resolveSingleAgentAuthorizationHeaderInternal(endpoint);
|
||||
}
|
||||
|
||||
Uri? resolveGatewayAcpEndpointInternal() {
|
||||
final target = assistantExecutionTargetForSession(
|
||||
sessionsControllerInternal.currentSessionKey,
|
||||
);
|
||||
if (target == AssistantExecutionTarget.singleAgent) {
|
||||
final remote = gatewayProfileBaseUriInternal(
|
||||
settings.primaryRemoteGatewayProfile,
|
||||
);
|
||||
if (remote != null) {
|
||||
return remote;
|
||||
}
|
||||
return gatewayProfileBaseUriInternal(settings.primaryLocalGatewayProfile);
|
||||
return null;
|
||||
}
|
||||
return gatewayProfileBaseUriInternal(
|
||||
gatewayProfileForAssistantExecutionTargetInternal(target),
|
||||
|
||||
@ -396,6 +396,16 @@ extension AppControllerDesktopWorkspaceExecution on AppController {
|
||||
);
|
||||
return;
|
||||
}
|
||||
final endpointOverride = resolveSingleAgentEndpointInternal(provider);
|
||||
if (endpointOverride == null) {
|
||||
await replaceSingleAgentThreadSkillsInternal(
|
||||
normalizedSessionKey,
|
||||
localSkills,
|
||||
);
|
||||
return;
|
||||
}
|
||||
final authorizationOverride =
|
||||
await resolveSingleAgentAuthorizationHeaderForProviderInternal(provider);
|
||||
await replaceSingleAgentThreadSkillsInternal(
|
||||
normalizedSessionKey,
|
||||
localSkills,
|
||||
@ -410,6 +420,8 @@ extension AppControllerDesktopWorkspaceExecution on AppController {
|
||||
'mode': 'single-agent',
|
||||
'provider': provider.providerId,
|
||||
},
|
||||
endpointOverride: endpointOverride,
|
||||
authorizationOverride: authorizationOverride,
|
||||
);
|
||||
final result = asMap(response['result']);
|
||||
final payload = result.isNotEmpty ? result : response;
|
||||
|
||||
@ -516,21 +516,8 @@ class SettingsSnapshot {
|
||||
ExternalAcpEndpointProfile externalAcpEndpointForProvider(
|
||||
SingleAgentProvider provider,
|
||||
) {
|
||||
final profile =
|
||||
externalAcpEndpointForProviderId(provider.providerId) ??
|
||||
return externalAcpEndpointForProviderId(provider.providerId) ??
|
||||
ExternalAcpEndpointProfile.defaultsForProvider(provider);
|
||||
final bridgeBaseUrl = acpBridgeBuiltinEndpointBaseUrl;
|
||||
if (provider.isAuto || bridgeBaseUrl.isEmpty) {
|
||||
return profile;
|
||||
}
|
||||
return profile.copyWith(endpoint: bridgeBaseUrl);
|
||||
}
|
||||
|
||||
String get acpBridgeBuiltinEndpointBaseUrl {
|
||||
if (!acpBridgeServerModeConfig.usesSelfHostedBase) {
|
||||
return '';
|
||||
}
|
||||
return acpBridgeServerModeConfig.selfHosted.serverUrl.trim();
|
||||
}
|
||||
|
||||
ExternalAcpEndpointProfile? externalAcpEndpointForProviderId(
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
@TestOn('vm')
|
||||
library;
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:xworkmate/app/app_controller.dart';
|
||||
@ -16,7 +14,7 @@ import 'app_controller_ai_gateway_chat_suite_fakes.dart';
|
||||
void main() {
|
||||
group('ACP bridge provider hub', () {
|
||||
test(
|
||||
'self-hosted ACP bridge base makes builtin single-agent providers visible without per-provider endpoints',
|
||||
'self-hosted ACP bridge base does not override builtin single-agent endpoints',
|
||||
() {
|
||||
final snapshot = SettingsSnapshot.defaults().copyWith(
|
||||
acpBridgeServerModeConfig: AcpBridgeServerModeConfig.defaults()
|
||||
@ -33,13 +31,13 @@ void main() {
|
||||
snapshot
|
||||
.externalAcpEndpointForProvider(SingleAgentProvider.codex)
|
||||
.endpoint,
|
||||
'https://bridge.example.com',
|
||||
'',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'builtin provider sync uses bridge base endpoint and self-hosted basic auth when endpoint auth is empty',
|
||||
'builtin provider sync does not inject self-hosted bridge endpoint or auth fallback',
|
||||
() async {
|
||||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||
final store = createIsolatedTestStore(enableSecureStorage: false);
|
||||
@ -69,6 +67,13 @@ void main() {
|
||||
username: 'review@example.com',
|
||||
),
|
||||
),
|
||||
externalAcpEndpoints: replaceExternalAcpEndpointForProvider(
|
||||
controller.settings.externalAcpEndpoints,
|
||||
SingleAgentProvider.opencode,
|
||||
controller.settings
|
||||
.externalAcpEndpointForProvider(SingleAgentProvider.opencode)
|
||||
.copyWith(endpoint: 'https://acp.example.com/opencode'),
|
||||
),
|
||||
),
|
||||
refreshAfterSave: false,
|
||||
);
|
||||
@ -79,11 +84,8 @@ void main() {
|
||||
(item) => item.providerId == 'opencode',
|
||||
);
|
||||
|
||||
expect(opencode.endpoint, 'https://bridge.example.com');
|
||||
expect(
|
||||
opencode.authorizationHeader,
|
||||
'Basic ${base64Encode(utf8.encode('review@example.com:top-secret'))}',
|
||||
);
|
||||
expect(opencode.endpoint, 'https://acp.example.com/opencode');
|
||||
expect(opencode.authorizationHeader, '');
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -85,6 +85,8 @@ void registerThreadSkillsAcpTests() {
|
||||
singleAgentTestSettingsInternal(
|
||||
workspacePath: tempDirectory.path,
|
||||
gatewayPort: acpServer.port,
|
||||
singleAgentProviderEndpoint:
|
||||
'http://127.0.0.1:${acpServer.port}/opencode',
|
||||
),
|
||||
);
|
||||
await store.saveTaskThreads(<TaskThread>[
|
||||
@ -218,6 +220,8 @@ void registerThreadSkillsAcpTests() {
|
||||
singleAgentTestSettingsInternal(
|
||||
workspacePath: tempDirectory.path,
|
||||
gatewayPort: acpServer.port,
|
||||
singleAgentProviderEndpoint:
|
||||
'http://127.0.0.1:${acpServer.port}/opencode',
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@ -66,6 +66,8 @@ class AcpSkillsStatusServerInternal {
|
||||
final HttpServer serverInternal;
|
||||
List<Map<String, dynamic>> skills;
|
||||
Map<String, dynamic>? skillsError;
|
||||
String? lastAuthorizationHeader;
|
||||
String? lastRequestPath;
|
||||
|
||||
int get port => serverInternal.port;
|
||||
|
||||
@ -97,6 +99,10 @@ class AcpSkillsStatusServerInternal {
|
||||
}
|
||||
|
||||
Future<void> handleRpcInternal(HttpRequest request) async {
|
||||
lastRequestPath = request.uri.path;
|
||||
lastAuthorizationHeader = request.headers.value(
|
||||
HttpHeaders.authorizationHeader,
|
||||
);
|
||||
final body = await utf8.decodeStream(request);
|
||||
final envelope = jsonDecode(body) as Map<String, dynamic>;
|
||||
final id = envelope['id'];
|
||||
|
||||
@ -56,6 +56,8 @@ Future<SecureConfigStore> createStoreInternal(String rootPath) async {
|
||||
SettingsSnapshot singleAgentTestSettingsInternal({
|
||||
required String workspacePath,
|
||||
int gatewayPort = 9,
|
||||
String singleAgentProviderEndpoint = '',
|
||||
String singleAgentProviderAuthRef = '',
|
||||
}) {
|
||||
final defaults = SettingsSnapshot.defaults();
|
||||
return defaults.copyWith(
|
||||
@ -78,5 +80,15 @@ SettingsSnapshot singleAgentTestSettingsInternal({
|
||||
),
|
||||
assistantExecutionTarget: AssistantExecutionTarget.singleAgent,
|
||||
workspacePath: workspacePath,
|
||||
externalAcpEndpoints: replaceExternalAcpEndpointForProvider(
|
||||
defaults.externalAcpEndpoints,
|
||||
SingleAgentProvider.opencode,
|
||||
defaults.externalAcpEndpointForProvider(
|
||||
SingleAgentProvider.opencode,
|
||||
).copyWith(
|
||||
endpoint: singleAgentProviderEndpoint,
|
||||
authRef: singleAgentProviderAuthRef,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -187,6 +187,28 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'routes generic ACP requests through the explicit hosted provider endpoint without fallback',
|
||||
() async {
|
||||
final server = await _AcpFakeServer.start(pathPrefix: '/gemini');
|
||||
addTearDown(server.close);
|
||||
|
||||
final client = GatewayAcpClient(
|
||||
endpointResolver: () => Uri.parse('http://127.0.0.1:9'),
|
||||
);
|
||||
|
||||
await client.request(
|
||||
method: 'skills.status',
|
||||
params: const <String, dynamic>{'provider': 'gemini'},
|
||||
endpointOverride: server.baseHttpUri,
|
||||
authorizationOverride: 'Bearer provider-secret',
|
||||
);
|
||||
|
||||
expect(server.lastHttpRequestPath, '/gemini/acp/rpc');
|
||||
expect(server.lastHttpAuthorization, 'Bearer provider-secret');
|
||||
},
|
||||
);
|
||||
|
||||
test('preserves hosted ACP base path for websocket requests', () async {
|
||||
final server = await _AcpFakeServer.start(pathPrefix: '/opencode');
|
||||
addTearDown(server.close);
|
||||
@ -498,6 +520,23 @@ class _AcpFakeServer {
|
||||
),
|
||||
);
|
||||
return;
|
||||
case 'skills.status':
|
||||
await respond(
|
||||
_resultEnvelope(
|
||||
id: id,
|
||||
result: const <String, dynamic>{
|
||||
'skills': <Map<String, dynamic>>[
|
||||
<String, dynamic>{
|
||||
'skillKey': 'gemini-remote',
|
||||
'name': 'Gemini Remote',
|
||||
'description': 'Hosted ACP skill payload',
|
||||
'source': 'acp',
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
);
|
||||
return;
|
||||
case 'session.cancel':
|
||||
await respond(
|
||||
_resultEnvelope(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user