Remove local CLI and provider mirror decisions

This commit is contained in:
Haitao Pan 2026-04-12 12:23:00 +08:00
parent 0505024e6d
commit 64e14beb70
19 changed files with 182 additions and 386 deletions

View File

@ -38,7 +38,6 @@ import '../runtime/agent_registry.dart';
import '../runtime/multi_agent_mounts.dart';
import '../runtime/multi_agent_orchestrator.dart';
import '../runtime/platform_environment.dart';
import '../runtime/single_agent_capabilities.dart';
import '../runtime/skill_directory_access.dart';
import 'task_thread_repositories.dart';
import 'app_controller_desktop_navigation.dart';
@ -294,9 +293,6 @@ class AppController extends ChangeNotifier {
GatewayAcpClient get gatewayAcpClientForTest => gatewayAcpClientInternal;
Map<SingleAgentProvider, SingleAgentCapabilities>
singleAgentCapabilitiesByProviderInternal =
const <SingleAgentProvider, SingleAgentCapabilities>{};
List<SingleAgentProvider> bridgeAdvertisedProvidersInternal =
const <SingleAgentProvider>[];
final Map<String, List<GatewayChatMessage>> assistantThreadMessagesInternal =
@ -439,7 +435,6 @@ class AppController extends ChangeNotifier {
bool isCodexBridgeBusyInternal = false;
String? codexBridgeErrorInternal;
String? codexRuntimeWarningInternal;
String? resolvedCodexCliPathInternal;
CodexCooperationState codexCooperationStateInternal =
CodexCooperationState.notStarted;
SettingsController get settingsController => settingsControllerInternal;
@ -528,9 +523,6 @@ class AppController extends ChangeNotifier {
bool get isCodexBridgeBusy => isCodexBridgeBusyInternal;
String? get codexBridgeError => codexBridgeErrorInternal;
String? get codexRuntimeWarning => codexRuntimeWarningInternal;
String? get resolvedCodexCliPath => resolvedCodexCliPathInternal;
bool get hasDetectedCodexCli => resolvedCodexCliPathInternal != null;
String get configuredCodexCliPath => settings.codexCliPath.trim();
CodeAgentRuntimeMode get configuredCodeAgentRuntimeMode =>
settings.codeAgentRuntimeMode;
CodeAgentRuntimeMode get effectiveCodeAgentRuntimeMode =>
@ -583,18 +575,13 @@ class AppController extends ChangeNotifier {
bridgeAdvertisedProvidersInternal,
);
List<SingleAgentProvider> get availableSingleAgentProviders =>
configuredSingleAgentProviders
.where(canUseSingleAgentProviderInternal)
.toList(growable: false);
List<AssistantExecutionTarget> visibleAssistantExecutionTargets(
Iterable<AssistantExecutionTarget> supportedTargets,
) {
final supported = supportedTargets.toSet();
final visible = <AssistantExecutionTarget>[];
if (supported.contains(AssistantExecutionTarget.singleAgent) &&
availableSingleAgentProviders.isNotEmpty) {
configuredSingleAgentProviders.isNotEmpty) {
visible.add(AssistantExecutionTarget.singleAgent);
}
if (supported.contains(AssistantExecutionTarget.gateway)) {
@ -612,25 +599,29 @@ class AppController extends ChangeNotifier {
];
}
bool get hasAnyAvailableSingleAgentProvider =>
availableSingleAgentProviders.isNotEmpty;
bool canUseSingleAgentProviderInternal(SingleAgentProvider provider) {
bool isBridgeAdvertisedSingleAgentProviderInternal(
SingleAgentProvider provider,
) {
if (provider.isUnspecified) {
return false;
}
final capabilities = singleAgentCapabilitiesByProviderInternal[provider];
return capabilities?.available == true &&
capabilities!.supportsProvider(provider);
return configuredSingleAgentProviders.any(
(item) => item.providerId == provider.providerId,
);
}
SingleAgentProvider? resolvedSingleAgentProviderInternal(
SingleAgentProvider? advertisedSingleAgentProviderInternal(
SingleAgentProvider selection,
) {
if (selection.isUnspecified) {
return null;
}
return canUseSingleAgentProviderInternal(selection) ? selection : null;
for (final provider in configuredSingleAgentProviders) {
if (provider.providerId == selection.providerId) {
return provider;
}
}
return null;
}
List<String> get aiGatewayConversationModelChoices {

View File

@ -75,7 +75,6 @@ Future<void> refreshAcpCapabilitiesRuntimeInternal(
.reconcile(
config: currentConfig,
aiGatewayUrl: controller.aiGatewayUrl,
configuredCodexCliPath: controller.configuredCodexCliPath,
);
if (jsonEncode(nextConfig.toJson()) != jsonEncode(currentConfig.toJson())) {
await controller.settingsControllerInternal.saveSnapshot(
@ -100,15 +99,6 @@ Future<void> refreshSingleAgentCapabilitiesRuntimeInternal(
);
controller.bridgeAdvertisedProvidersInternal =
normalizeSingleAgentProviderList(capabilities.providerCatalog);
final next = <SingleAgentProvider, SingleAgentCapabilities>{};
for (final provider in controller.bridgeAdvertisedProvidersInternal) {
next[provider] = SingleAgentCapabilities(
available: true,
supportedProviders: <SingleAgentProvider>[provider],
endpoint: 'go-task-service',
);
}
controller.singleAgentCapabilitiesByProviderInternal = next;
if (!controller.disposedInternal) {
controller.notifyListeners();
}
@ -248,8 +238,6 @@ CodeAgentNodeState buildCodeAgentNodeStateRuntimeInternal(
bridgeEnabled: controller.isCodexBridgeEnabledInternal,
bridgeState: controller.codexCooperationStateInternal.name,
preferredProviderId: 'codex',
resolvedCodexCliPath: controller.resolvedCodexCliPathInternal,
configuredCodexCliPath: controller.configuredCodexCliPath,
);
}
@ -313,11 +301,6 @@ Future<void> ensureCodexGatewayRegistrationRuntimeInternal(
'providerId': 'codex',
'runtimeMode': controller.effectiveCodeAgentRuntimeMode.name,
'gatewayMode': bridgeGatewayModeRuntimeInternal(controller).name,
'binaryConfigured':
(controller.resolvedCodexCliPath ??
controller.configuredCodexCliPath)
.trim()
.isNotEmpty,
'capabilities': const <String>[
'chat',
'code-edit',

View File

@ -242,7 +242,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
sessionsControllerInternal.currentSessionKey,
);
final provider =
resolvedSingleAgentProviderInternal(selection) ?? selection;
advertisedSingleAgentProviderInternal(selection) ?? selection;
final providerLabel = provider.isUnspecified
? appText('Bridge Provider', 'Bridge Provider')
: provider.label;
@ -436,14 +436,11 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
'Built-in Codex runtime is reserved for a future release; XWorkmate switched back to External Codex CLI automatically.',
)
: null;
final normalizedPath = snapshot.codexCliPath.trim();
if (normalizedPath == snapshot.codexCliPath &&
normalizedRuntimeMode == snapshot.codeAgentRuntimeMode) {
if (normalizedRuntimeMode == snapshot.codeAgentRuntimeMode) {
return snapshot;
}
return snapshot.copyWith(
codeAgentRuntimeMode: normalizedRuntimeMode,
codexCliPath: normalizedPath,
);
}
@ -463,36 +460,6 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
forceRefresh: forceRefresh,
);
Future<void> refreshResolvedCodexCliPathInternal() async {
if (effectiveCodeAgentRuntimeMode != CodeAgentRuntimeMode.externalCli) {
resolvedCodexCliPathInternal = null;
return;
}
if (shouldBlockEmbeddedAgentLaunch(
isAppleHost: Platform.isIOS || Platform.isMacOS,
)) {
resolvedCodexCliPathInternal = null;
return;
}
final configuredPath = configuredCodexCliPath;
String? detectedPath;
if (configuredPath.isNotEmpty) {
try {
if (await File(configuredPath).exists()) {
detectedPath = configuredPath;
}
} catch (_) {
detectedPath = null;
}
}
detectedPath ??= await runtimeCoordinatorInternal.codex.findCodexBinary();
if (disposedInternal) {
return;
}
resolvedCodexCliPathInternal = detectedPath;
}
List<ManagedMountTargetState> mergeAcpCapabilitiesIntoMountTargetsInternal(
List<ManagedMountTargetState> current,
GatewayAcpCapabilities capabilities,
@ -637,9 +604,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
}
setActiveAppLanguage(current.appLanguage);
multiAgentOrchestratorInternal.updateConfig(current.multiAgent);
if (previous.codexCliPath != current.codexCliPath ||
previous.codeAgentRuntimeMode != current.codeAgentRuntimeMode) {
await refreshResolvedCodexCliPathInternal();
if (previous.codeAgentRuntimeMode != current.codeAgentRuntimeMode) {
registerCodexExternalProviderInternal();
if (disposedInternal) {
return;

View File

@ -526,7 +526,6 @@ extension AppControllerDesktopSettingsRuntime on AppController {
await desktopPlatformServiceInternal.setLaunchAtLogin(
settings.launchAtLogin,
);
await refreshResolvedCodexCliPathInternal();
registerCodexExternalProviderInternal();
await refreshSingleAgentCapabilitiesInternal();
await refreshAcpCapabilitiesInternal(persistMountTargets: true);
@ -787,9 +786,7 @@ extension AppControllerDesktopSettingsRuntime on AppController {
if (disposedInternal) {
return;
}
if (previous.codexCliPath != current.codexCliPath ||
previous.codeAgentRuntimeMode != current.codeAgentRuntimeMode) {
await refreshResolvedCodexCliPathInternal();
if (previous.codeAgentRuntimeMode != current.codeAgentRuntimeMode) {
registerCodexExternalProviderInternal();
}
unawaited(refreshSingleAgentCapabilitiesInternal().catchError((_) {}));

View File

@ -57,9 +57,6 @@ Future<void> sendSingleAgentMessageDesktopGoTaskFlowInternal(
sessionKey,
);
final selection = controller.singleAgentProviderForSession(sessionKey);
final effectiveProvider =
controller.resolvedSingleAgentProviderInternal(selection) ??
selection;
final preflightWorkingDirectory = controller
.resolveSingleAgentWorkingDirectoryForSessionInternal(sessionKey);
if (preflightWorkingDirectory == null ||
@ -79,8 +76,50 @@ Future<void> sendSingleAgentMessageDesktopGoTaskFlowInternal(
);
throw error;
}
if (controller.resolveExternalAcpEndpointForTargetInternal(
AssistantExecutionTarget.singleAgent,
) ==
null) {
controller.upsertTaskThreadInternal(
sessionKey,
lifecycleStatus: 'ready',
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
lastResultCode: 'error',
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
);
controller.appendAssistantThreadMessageInternal(
sessionKey,
assistantErrorMessageSingleAgentDesktopInternal(
controller,
appText(
'Bridge ACP 入口当前不可用。',
'The bridge ACP entrypoint is currently unavailable.',
),
),
);
return;
}
final routingResolution = await controller.goTaskServiceClientInternal
.resolveExternalAcpRouting(
taskPrompt: message,
workingDirectory: preflightWorkingDirectory,
routing: routing,
);
final resolvedProvider = SingleAgentProviderCopy.fromJsonValue(
routingResolution.resolvedProviderId,
);
final effectiveProvider = !resolvedProvider.isUnspecified
? resolvedProvider
: controller.advertisedSingleAgentProviderInternal(selection) ??
selection;
final unavailableReason =
controller.singleAgentShouldSuggestAcpSwitchForSession(sessionKey)
routingResolution.unavailable
? singleAgentUnavailableLabelDesktopInternal(
controller,
sessionKey,
routingResolution.unavailableMessage,
)
: controller.singleAgentShouldSuggestAcpSwitchForSession(sessionKey)
? singleAgentUnavailableLabelDesktopInternal(
controller,
sessionKey,
@ -95,14 +134,6 @@ Future<void> sendSingleAgentMessageDesktopGoTaskFlowInternal(
'The bridge does not currently have any synced providers.',
),
)
: controller.resolveExternalAcpEndpointForTargetInternal(
AssistantExecutionTarget.singleAgent,
) ==
null
? appText(
'Bridge ACP 入口当前不可用。',
'The bridge ACP entrypoint is currently unavailable.',
)
: null;
if (unavailableReason != null) {
controller.upsertTaskThreadInternal(

View File

@ -269,7 +269,7 @@ extension AppControllerDesktopThreadSessions on AppController {
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
sessionKey,
);
return resolvedSingleAgentProviderInternal(
return advertisedSingleAgentProviderInternal(
singleAgentProviderForSession(normalizedSessionKey),
);
}
@ -285,7 +285,7 @@ extension AppControllerDesktopThreadSessions on AppController {
AssistantExecutionTarget.singleAgent) {
return false;
}
return !hasAnyAvailableSingleAgentProvider;
return configuredSingleAgentProviders.isEmpty;
}
bool get currentSingleAgentNeedsBridgeProvider =>
@ -310,8 +310,8 @@ extension AppControllerDesktopThreadSessions on AppController {
if (selection.isUnspecified) {
return false;
}
return !canUseSingleAgentProviderInternal(selection) &&
hasAnyAvailableSingleAgentProvider;
return !isBridgeAdvertisedSingleAgentProviderInternal(selection) &&
configuredSingleAgentProviders.isNotEmpty;
}
bool get currentSingleAgentShouldSuggestAcpSwitch =>
@ -371,9 +371,7 @@ extension AppControllerDesktopThreadSessions on AppController {
singleAgentShouldShowModelControlForSession(currentSessionKey);
List<SingleAgentProvider> get singleAgentProviderOptions =>
availableSingleAgentProviders.isNotEmpty
? availableSingleAgentProviders
: configuredSingleAgentProviders;
configuredSingleAgentProviders;
String singleAgentProviderLabelForSession(String sessionKey) {
return singleAgentProviderForSession(sessionKey).label;

View File

@ -79,7 +79,6 @@ Future<void> refreshMultiAgentMountsThreadSessionInternal(
var nextConfig = await controller.multiAgentMountManagerInternal.reconcile(
config: effectiveConfig,
aiGatewayUrl: controller.aiGatewayUrl,
configuredCodexCliPath: controller.configuredCodexCliPath,
);
if (nextConfig.autoSync != currentConfig.autoSync) {
nextConfig = nextConfig.copyWith(autoSync: currentConfig.autoSync);
@ -354,12 +353,6 @@ List<String> assistantModelChoicesForSessionThreadSessionInternal(
normalizedSessionKey,
);
if (target == AssistantExecutionTarget.singleAgent) {
final singleAgentUsesAiGatewayFallback =
!controller.hasAnyAvailableSingleAgentProvider &&
controller.canUseAiGatewayConversation;
if (singleAgentUsesAiGatewayFallback) {
return controller.aiGatewayConversationModelChoices;
}
final runtimeModel = controller.singleAgentRuntimeModelForSession(
normalizedSessionKey,
);

View File

@ -663,7 +663,7 @@ class _SkillsPanel extends StatelessWidget {
? StatusInfo(appText('当前模式', 'Current mode'), StatusTone.accent)
: StatusInfo(appText('可切换', 'Available'), StatusTone.success),
chips: [
for (final provider in controller.availableSingleAgentProviders)
for (final provider in controller.configuredSingleAgentProviders)
provider.label,
],
skills: singleAgentSkills.map((item) => item.name).toList(),

View File

@ -12,8 +12,6 @@ class CodeAgentNodeState {
required this.bridgeEnabled,
required this.bridgeState,
required this.preferredProviderId,
this.resolvedCodexCliPath,
this.configuredCodexCliPath = '',
});
final String selectedAgentId;
@ -23,8 +21,6 @@ class CodeAgentNodeState {
final bool bridgeEnabled;
final String bridgeState;
final String preferredProviderId;
final String? resolvedCodexCliPath;
final String configuredCodexCliPath;
}
/// Resolved gateway dispatch envelope for the app-mediated node.
@ -58,8 +54,6 @@ class CodeAgentNodeOrchestrator {
'runtimeMode': state.runtimeMode.name,
'bridgeEnabled': state.bridgeEnabled,
'bridgeState': state.bridgeState,
'resolvedCodexCliPath': state.resolvedCodexCliPath?.trim() ?? '',
'configuredCodexCliPath': state.configuredCodexCliPath.trim(),
},
nodeInfo: const <String, dynamic>{
'id': 'xworkmate-app',
@ -82,9 +76,6 @@ class CodeAgentNodeOrchestrator {
)
: null;
final normalizedAgentId = state.selectedAgentId.trim();
final configuredPath = state.resolvedCodexCliPath?.trim().isNotEmpty == true
? state.resolvedCodexCliPath!.trim()
: state.configuredCodexCliPath.trim();
final metadata = <String, dynamic>{
'node': <String, dynamic>{
@ -107,7 +98,6 @@ class CodeAgentNodeOrchestrator {
CodeAgentRuntimeMode.externalCli => 'stdio-jsonrpc',
CodeAgentRuntimeMode.builtIn => 'ffi-runtime',
},
if (configuredPath.isNotEmpty) 'binaryConfigured': true,
},
if (provider != null)
'provider': <String, dynamic>{

View File

@ -16,7 +16,6 @@ class GoMultiAgentMountDesktopClient implements MultiAgentMountResolver {
Future<MultiAgentConfig?> reconcile({
required MultiAgentConfig config,
required String aiGatewayUrl,
String configuredCodexCliPath = '',
required String codexHome,
required String opencodeHome,
required ArisMountProbe arisProbe,
@ -32,7 +31,6 @@ class GoMultiAgentMountDesktopClient implements MultiAgentMountResolver {
.toList(growable: false),
},
'aiGatewayUrl': aiGatewayUrl.trim(),
'configuredCodexCliPath': configuredCodexCliPath.trim(),
'codexHome': codexHome.trim(),
'opencodeHome': opencodeHome.trim(),
'aris': arisProbe.toJson(),

View File

@ -40,7 +40,6 @@ abstract class MultiAgentMountResolver {
Future<MultiAgentConfig?> reconcile({
required MultiAgentConfig config,
required String aiGatewayUrl,
String configuredCodexCliPath = '',
required String codexHome,
required String opencodeHome,
required ArisMountProbe arisProbe,

View File

@ -52,12 +52,10 @@ class MultiAgentMountManager {
Future<MultiAgentConfig> reconcile({
required MultiAgentConfig config,
required String aiGatewayUrl,
String configuredCodexCliPath = '',
}) async {
final resolved = await _resolver?.reconcile(
config: config,
aiGatewayUrl: aiGatewayUrl,
configuredCodexCliPath: configuredCodexCliPath,
codexHome: _codexConfigBridge.codexHome,
opencodeHome: _opencodeConfigBridge.opencodeHome,
arisProbe: await _buildArisProbe(),
@ -68,7 +66,6 @@ class MultiAgentMountManager {
return _reconcileLocally(
config: config,
aiGatewayUrl: aiGatewayUrl,
configuredCodexCliPath: configuredCodexCliPath,
);
}
@ -94,7 +91,6 @@ class MultiAgentMountManager {
Future<MultiAgentConfig> _reconcileLocally({
required MultiAgentConfig config,
required String aiGatewayUrl,
String configuredCodexCliPath = '',
}) async {
final states = <ManagedMountTargetState>[];
for (final adapter in _adapters) {
@ -103,7 +99,6 @@ class MultiAgentMountManager {
await adapter.reconcile(
config: config,
aiGatewayUrl: aiGatewayUrl,
configuredCodexCliPath: configuredCodexCliPath,
),
);
} catch (error) {
@ -115,9 +110,7 @@ class MultiAgentMountManager {
supportsMcp: adapter.supportsMcp,
supportsAiGatewayInjection: adapter.supportsAiGatewayInjection,
).copyWith(
available: await adapter.isInstalled(
configuredCodexCliPath: configuredCodexCliPath,
),
available: await adapter.isInstalled(),
discoveryState: 'error',
syncState: 'error',
detail: error.toString(),
@ -150,12 +143,11 @@ abstract class CliMountAdapter {
bool get supportsMcp;
bool get supportsAiGatewayInjection;
Future<bool> isInstalled({String configuredCodexCliPath = ''});
Future<bool> isInstalled();
Future<ManagedMountTargetState> reconcile({
required MultiAgentConfig config,
required String aiGatewayUrl,
String configuredCodexCliPath = '',
});
Future<String> _runCommand(List<String> command) async {
@ -188,15 +180,6 @@ abstract class CliMountAdapter {
.length;
}
Future<bool> _binaryExists(String command) async {
final check = await Process.run(
Platform.isWindows ? 'where' : 'which',
<String>[command],
runInShell: true,
);
return check.exitCode == 0 && '${check.stdout}'.trim().isNotEmpty;
}
int countMcpTomlSections(String content) {
return RegExp(
r'^\[mcp_servers\.[^\]]+\]',
@ -230,7 +213,7 @@ class ArisMountAdapter extends CliMountAdapter {
bool get supportsAiGatewayInjection => false;
@override
Future<bool> isInstalled({String configuredCodexCliPath = ''}) async {
Future<bool> isInstalled() async {
try {
await _bundleRepository.loadManifest();
return true;
@ -243,7 +226,6 @@ class ArisMountAdapter extends CliMountAdapter {
Future<ManagedMountTargetState> reconcile({
required MultiAgentConfig config,
required String aiGatewayUrl,
String configuredCodexCliPath = '',
}) async {
try {
final bundle = await _bundleRepository.ensureReady();
@ -313,23 +295,14 @@ class CodexMountAdapter extends CliMountAdapter {
bool get supportsAiGatewayInjection => true;
@override
Future<bool> isInstalled({String configuredCodexCliPath = ''}) async {
final configuredPath = configuredCodexCliPath.trim();
if (configuredPath.isNotEmpty && await File(configuredPath).exists()) {
return true;
}
return _binaryExists('codex');
}
Future<bool> isInstalled() async => false;
@override
Future<ManagedMountTargetState> reconcile({
required MultiAgentConfig config,
required String aiGatewayUrl,
String configuredCodexCliPath = '',
}) async {
final available = await isInstalled(
configuredCodexCliPath: configuredCodexCliPath,
);
final available = await isInstalled();
final configFile = File('${_bridge.codexHome}/config.toml');
final content = await configFile.exists()
? await configFile.readAsString()
@ -391,14 +364,12 @@ class ClaudeMountAdapter extends CliMountAdapter {
bool get supportsAiGatewayInjection => true;
@override
Future<bool> isInstalled({String configuredCodexCliPath = ''}) =>
_binaryExists('claude');
Future<bool> isInstalled() async => false;
@override
Future<ManagedMountTargetState> reconcile({
required MultiAgentConfig config,
required String aiGatewayUrl,
String configuredCodexCliPath = '',
}) async {
final available = await isInstalled();
final discoveredMcpCount = available
@ -441,14 +412,12 @@ class GeminiMountAdapter extends CliMountAdapter {
bool get supportsAiGatewayInjection => true;
@override
Future<bool> isInstalled({String configuredCodexCliPath = ''}) =>
_binaryExists('gemini');
Future<bool> isInstalled() async => false;
@override
Future<ManagedMountTargetState> reconcile({
required MultiAgentConfig config,
required String aiGatewayUrl,
String configuredCodexCliPath = '',
}) async {
final available = await isInstalled();
final discoveredMcpCount = available
@ -495,14 +464,12 @@ class OpencodeMountAdapter extends CliMountAdapter {
bool get supportsAiGatewayInjection => true;
@override
Future<bool> isInstalled({String configuredCodexCliPath = ''}) =>
_binaryExists('opencode');
Future<bool> isInstalled() async => false;
@override
Future<ManagedMountTargetState> reconcile({
required MultiAgentConfig config,
required String aiGatewayUrl,
String configuredCodexCliPath = '',
}) async {
final available = await isInstalled();
final content = await _bridge.readConfig();
@ -562,14 +529,12 @@ class OpenClawMountAdapter extends CliMountAdapter {
bool get supportsAiGatewayInjection => true;
@override
Future<bool> isInstalled({String configuredCodexCliPath = ''}) =>
_binaryExists('openclaw');
Future<bool> isInstalled() async => false;
@override
Future<ManagedMountTargetState> reconcile({
required MultiAgentConfig config,
required String aiGatewayUrl,
String configuredCodexCliPath = '',
}) async {
final available = await isInstalled();
final configFile = File(

View File

@ -425,14 +425,12 @@ class AcpBridgeServerAdvancedOverrides {
required this.gatewayProfiles,
required this.vault,
required this.aiGateway,
required this.acpBridgeServerProfiles,
required this.authorizedSkillDirectories,
});
final List<GatewayConnectionProfile> gatewayProfiles;
final VaultConfig vault;
final AiGatewayProfile aiGateway;
final List<ExternalAcpEndpointProfile> acpBridgeServerProfiles;
final List<AuthorizedSkillDirectory> authorizedSkillDirectories;
factory AcpBridgeServerAdvancedOverrides.defaults() {
@ -440,7 +438,6 @@ class AcpBridgeServerAdvancedOverrides {
gatewayProfiles: normalizeGatewayProfiles(),
vault: VaultConfig.defaults(),
aiGateway: AiGatewayProfile.defaults(),
acpBridgeServerProfiles: normalizeExternalAcpEndpoints(),
authorizedSkillDirectories: normalizeAuthorizedSkillDirectories(),
);
}
@ -449,7 +446,6 @@ class AcpBridgeServerAdvancedOverrides {
List<GatewayConnectionProfile>? gatewayProfiles,
VaultConfig? vault,
AiGatewayProfile? aiGateway,
List<ExternalAcpEndpointProfile>? acpBridgeServerProfiles,
List<AuthorizedSkillDirectory>? authorizedSkillDirectories,
}) {
return AcpBridgeServerAdvancedOverrides(
@ -458,9 +454,6 @@ class AcpBridgeServerAdvancedOverrides {
: this.gatewayProfiles,
vault: vault ?? this.vault,
aiGateway: aiGateway ?? this.aiGateway,
acpBridgeServerProfiles: acpBridgeServerProfiles != null
? normalizeExternalAcpEndpoints(profiles: acpBridgeServerProfiles)
: this.acpBridgeServerProfiles,
authorizedSkillDirectories: authorizedSkillDirectories != null
? normalizeAuthorizedSkillDirectories(
directories: authorizedSkillDirectories,
@ -476,9 +469,6 @@ class AcpBridgeServerAdvancedOverrides {
.toList(growable: false),
'vault': vault.toJson(),
'aiGateway': aiGateway.toJson(),
'acpBridgeServerProfiles': acpBridgeServerProfiles
.map((item) => item.toJson())
.toList(growable: false),
'authorizedSkillDirectories': authorizedSkillDirectories
.map((item) => item.toJson())
.toList(growable: false),
@ -502,16 +492,6 @@ class AcpBridgeServerAdvancedOverrides {
aiGateway: AiGatewayProfile.fromJson(
(json['aiGateway'] as Map?)?.cast<String, dynamic>() ?? const {},
),
acpBridgeServerProfiles: normalizeExternalAcpEndpoints(
profiles:
((json['acpBridgeServerProfiles'] as List?) ?? const <Object>[])
.whereType<Map>()
.map(
(item) => ExternalAcpEndpointProfile.fromJson(
item.cast<String, dynamic>(),
),
),
),
authorizedSkillDirectories: normalizeAuthorizedSkillDirectories(
directories:
((json['authorizedSkillDirectories'] as List?) ?? const <Object>[])

View File

@ -24,11 +24,9 @@ class SettingsSnapshot {
required this.remoteProjectRoot,
required this.cliPath,
required this.codeAgentRuntimeMode,
required this.codexCliPath,
required this.defaultModel,
required this.defaultProvider,
required this.gatewayProfiles,
required this.providerSyncDefinitions,
required this.authorizedSkillDirectories,
required this.ollamaLocal,
required this.ollamaCloud,
@ -59,11 +57,9 @@ class SettingsSnapshot {
final String remoteProjectRoot;
final String cliPath;
final CodeAgentRuntimeMode codeAgentRuntimeMode;
final String codexCliPath;
final String defaultModel;
final String defaultProvider;
final List<GatewayConnectionProfile> gatewayProfiles;
final List<ExternalAcpEndpointProfile> providerSyncDefinitions;
final List<AuthorizedSkillDirectory> authorizedSkillDirectories;
final OllamaLocalConfig ollamaLocal;
final OllamaCloudConfig ollamaCloud;
@ -95,11 +91,9 @@ class SettingsSnapshot {
remoteProjectRoot: '',
cliPath: 'openclaw',
codeAgentRuntimeMode: CodeAgentRuntimeMode.externalCli,
codexCliPath: '',
defaultModel: '',
defaultProvider: 'gateway',
gatewayProfiles: normalizeGatewayProfiles(),
providerSyncDefinitions: normalizeExternalAcpEndpoints(),
authorizedSkillDirectories: normalizeAuthorizedSkillDirectories(),
ollamaLocal: OllamaLocalConfig.defaults(),
ollamaCloud: OllamaCloudConfig.defaults(),
@ -132,11 +126,9 @@ class SettingsSnapshot {
String? remoteProjectRoot,
String? cliPath,
CodeAgentRuntimeMode? codeAgentRuntimeMode,
String? codexCliPath,
String? defaultModel,
String? defaultProvider,
List<GatewayConnectionProfile>? gatewayProfiles,
List<ExternalAcpEndpointProfile>? providerSyncDefinitions,
List<AuthorizedSkillDirectory>? authorizedSkillDirectories,
OllamaLocalConfig? ollamaLocal,
OllamaCloudConfig? ollamaCloud,
@ -160,9 +152,6 @@ class SettingsSnapshot {
final resolvedGatewayProfiles = gatewayProfiles != null
? normalizeGatewayProfiles(profiles: gatewayProfiles)
: this.gatewayProfiles;
final resolvedProviderSyncDefinitions = providerSyncDefinitions != null
? normalizeExternalAcpEndpoints(profiles: providerSyncDefinitions)
: this.providerSyncDefinitions;
final resolvedAuthorizedSkillDirectories =
authorizedSkillDirectories != null
? normalizeAuthorizedSkillDirectories(
@ -179,11 +168,9 @@ class SettingsSnapshot {
remoteProjectRoot: remoteProjectRoot ?? this.remoteProjectRoot,
cliPath: cliPath ?? this.cliPath,
codeAgentRuntimeMode: codeAgentRuntimeMode ?? this.codeAgentRuntimeMode,
codexCliPath: codexCliPath ?? this.codexCliPath,
defaultModel: defaultModel ?? this.defaultModel,
defaultProvider: defaultProvider ?? this.defaultProvider,
gatewayProfiles: resolvedGatewayProfiles,
providerSyncDefinitions: resolvedProviderSyncDefinitions,
authorizedSkillDirectories: resolvedAuthorizedSkillDirectories,
ollamaLocal: ollamaLocal ?? this.ollamaLocal,
ollamaCloud: ollamaCloud ?? this.ollamaCloud,
@ -222,15 +209,11 @@ class SettingsSnapshot {
'remoteProjectRoot': remoteProjectRoot,
'cliPath': cliPath,
'codeAgentRuntimeMode': codeAgentRuntimeMode.name,
'codexCliPath': codexCliPath,
'defaultModel': defaultModel,
'defaultProvider': defaultProvider,
'gatewayProfiles': gatewayProfiles
.map((item) => item.toJson())
.toList(growable: false),
'providerSyncDefinitions': providerSyncDefinitions
.map((item) => item.toJson())
.toList(growable: false),
'authorizedSkillDirectories': authorizedSkillDirectories
.map((item) => item.toJson())
.toList(growable: false),
@ -270,15 +253,6 @@ class SettingsSnapshot {
GatewayConnectionProfile.fromJson(item.cast<String, dynamic>()),
),
);
final providerSyncDefinitions = normalizeExternalAcpEndpoints(
profiles: ((json['providerSyncDefinitions'] as List?) ?? const <Object>[])
.whereType<Map>()
.map(
(item) => ExternalAcpEndpointProfile.fromJson(
item.cast<String, dynamic>(),
),
),
);
final authorizedSkillDirectories = normalizeAuthorizedSkillDirectories(
directories:
((json['authorizedSkillDirectories'] as List?) ?? const <Object>[])
@ -308,9 +282,6 @@ class SettingsSnapshot {
codeAgentRuntimeMode: CodeAgentRuntimeModeCopy.fromJsonValue(
json['codeAgentRuntimeMode'] as String?,
),
codexCliPath:
json['codexCliPath'] as String? ??
SettingsSnapshot.defaults().codexCliPath,
defaultModel:
json['defaultModel'] as String? ??
SettingsSnapshot.defaults().defaultModel,
@ -318,7 +289,6 @@ class SettingsSnapshot {
json['defaultProvider'] as String? ??
SettingsSnapshot.defaults().defaultProvider,
gatewayProfiles: gatewayProfiles,
providerSyncDefinitions: providerSyncDefinitions,
authorizedSkillDirectories: authorizedSkillDirectories,
ollamaLocal: OllamaLocalConfig.fromJson(
(json['ollamaLocal'] as Map?)?.cast<String, dynamic>() ?? const {},
@ -419,91 +389,15 @@ class SettingsSnapshot {
return copyWithGatewayProfileAt(index, profile);
}
ExternalAcpEndpointProfile providerSyncDefinitionForProvider(
SingleAgentProvider provider,
) {
return providerSyncDefinitionForProviderId(provider.providerId) ??
ExternalAcpEndpointProfile.defaultsForProvider(provider);
}
ExternalAcpEndpointProfile? providerSyncDefinitionForProviderId(
String providerId,
) {
final normalized = normalizeSingleAgentProviderId(providerId);
if (normalized.isEmpty) {
return null;
}
for (final item in providerSyncDefinitions) {
if (item.providerKey == normalized) {
return item;
}
}
return null;
}
SingleAgentProvider resolveSingleAgentProvider(SingleAgentProvider provider) {
if (provider.isUnspecified) {
return SingleAgentProvider.unspecified;
}
final profile = providerSyncDefinitionForProviderId(provider.providerId);
if (profile != null) {
return profile.toProvider();
}
return provider;
}
SingleAgentProvider singleAgentProviderForId(String providerId) {
final resolved = normalizeSingleAgentProviderId(providerId);
if (resolved.isEmpty || resolved == 'auto') {
return SingleAgentProvider.unspecified;
}
final normalizedSelection = SingleAgentProvider.fromJsonValue(resolved);
final profile = providerSyncDefinitionForProviderId(
normalizedSelection.providerId,
);
if (profile != null) {
return profile.toProvider();
}
return normalizedSelection;
}
SingleAgentProvider sanitizeSingleAgentProviderSelection(
SingleAgentProvider provider,
) {
final resolved = resolveSingleAgentProvider(provider);
if (resolved.isUnspecified) {
if (provider.isUnspecified) {
return SingleAgentProvider.unspecified;
}
if (isBridgeOwnedSingleAgentProviderId(resolved.providerId)) {
return resolved;
if (isBridgeOwnedSingleAgentProviderId(provider.providerId)) {
return provider;
}
return SingleAgentProvider.unspecified;
}
SettingsSnapshot copyWithProviderSyncDefinitionForProvider(
SingleAgentProvider provider,
ExternalAcpEndpointProfile profile,
) {
return copyWith(
providerSyncDefinitions: replaceExternalAcpEndpointForProvider(
providerSyncDefinitions,
provider,
profile,
),
);
}
SettingsSnapshot captureAcpBridgeServerAdvancedOverrides() {
return copyWith(
acpBridgeServerModeConfig: acpBridgeServerModeConfig.copyWith(
advancedOverrides: AcpBridgeServerAdvancedOverrides(
gatewayProfiles: gatewayProfiles,
vault: vault,
aiGateway: aiGateway,
acpBridgeServerProfiles: providerSyncDefinitions,
authorizedSkillDirectories: authorizedSkillDirectories,
),
),
);
}
}

View File

@ -10,7 +10,6 @@ import 'package:xworkmate/runtime/desktop_platform_service.dart';
import 'package:xworkmate/runtime/go_task_service_client.dart';
import 'package:xworkmate/runtime/runtime_models.dart';
import 'package:xworkmate/runtime/secure_config_store.dart';
import 'package:xworkmate/runtime/single_agent_capabilities.dart';
import 'package:xworkmate/runtime/skill_directory_access.dart';
void main() {
@ -49,7 +48,63 @@ void main() {
reason:
'app-side runtime coordination should not own provider auth side-channels',
);
expect(
source.contains('configuredCodexCliPath'),
isFalse,
reason:
'runtime coordination should not pass configured Codex CLI paths into runtime flows',
);
expect(
source.contains('resolvedCodexCliPath'),
isFalse,
reason:
'runtime coordination should not retain detected Codex CLI paths',
);
}
final settingsSnapshot = File('lib/runtime/runtime_models_settings_snapshot.dart')
.readAsStringSync();
expect(
settingsSnapshot.contains('providerSyncDefinitions'),
isFalse,
reason:
'settings snapshots should not persist provider catalog mirror data',
);
expect(
settingsSnapshot.contains('codexCliPath'),
isFalse,
reason: 'settings snapshots should not persist app-side Codex CLI paths',
);
final accountModels = File('lib/runtime/runtime_models_account.dart')
.readAsStringSync();
expect(
accountModels.contains('acpBridgeServerProfiles'),
isFalse,
reason:
'account advanced overrides should not mirror bridge provider catalogs',
);
final orchestrator = File('lib/runtime/code_agent_node_orchestrator.dart')
.readAsStringSync();
expect(
orchestrator.contains('configuredCodexCliPath'),
isFalse,
reason:
'node metadata should not expose configured Codex CLI paths anymore',
);
expect(
orchestrator.contains('resolvedCodexCliPath'),
isFalse,
reason:
'node metadata should not expose detected Codex CLI paths anymore',
);
expect(
orchestrator.contains('binaryConfigured'),
isFalse,
reason:
'node metadata should not derive binaryConfigured from local CLI detection',
);
});
test(
@ -123,20 +178,14 @@ void main() {
controller.dispose();
await server.close(force: true);
if (root.existsSync()) {
await root.delete(recursive: true);
try {
await root.delete(recursive: true);
} catch (_) {}
}
});
final endpoint = 'http://${server.address.address}:${server.port}';
final nextSettings = controller.settings.copyWith(
providerSyncDefinitions: <ExternalAcpEndpointProfile>[
ExternalAcpEndpointProfile.defaultsForProvider(
SingleAgentProvider.codex,
).copyWith(endpoint: endpoint),
],
);
controller.settingsController.snapshotInternal = nextSettings;
controller.lastObservedSettingsSnapshotInternal = nextSettings;
controller.settingsController.snapshotInternal = controller.settings;
controller.lastObservedSettingsSnapshotInternal = controller.settings;
const sessionKey = 'draft:runtime-cleanup';
controller.initializeAssistantThreadContext(
@ -204,7 +253,9 @@ void main() {
addTearDown(() async {
controller.dispose();
if (root.existsSync()) {
await root.delete(recursive: true);
try {
await root.delete(recursive: true);
} catch (_) {}
}
});
@ -243,14 +294,6 @@ void _seedBridgeProviders(
List<SingleAgentProvider> providers,
) {
controller.bridgeAdvertisedProvidersInternal = providers;
controller.singleAgentCapabilitiesByProviderInternal = {
for (final provider in providers)
provider: SingleAgentCapabilities(
available: true,
supportedProviders: <SingleAgentProvider>[provider],
endpoint: 'bridge',
),
};
}
class _FakeSkillDirectoryAccessService implements SkillDirectoryAccessService {

View File

@ -9,7 +9,6 @@ import 'package:xworkmate/app/app_controller_desktop_workspace_execution.dart';
import 'package:xworkmate/runtime/go_task_service_client.dart';
import 'package:xworkmate/runtime/runtime_models.dart';
import 'package:xworkmate/runtime/secure_config_store.dart';
import 'package:xworkmate/runtime/single_agent_capabilities.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
@ -94,9 +93,9 @@ void main() {
);
expect(
client.resolveExternalAcpRoutingCallCount,
0,
2,
reason:
'single-agent turns should go straight to session.start/session.message without app-side routing preflight',
'single-agent turns should preflight through bridge routing.resolve once per turn before dispatch',
);
},
);
@ -218,14 +217,6 @@ void _seedBridgeProviders(
List<SingleAgentProvider> providers,
) {
controller.bridgeAdvertisedProvidersInternal = providers;
controller.singleAgentCapabilitiesByProviderInternal = {
for (final provider in providers)
provider: SingleAgentCapabilities(
available: true,
supportedProviders: <SingleAgentProvider>[provider],
endpoint: 'bridge',
),
};
}
class _CapturingGoTaskServiceClient implements GoTaskServiceClient {

View File

@ -11,7 +11,6 @@ import 'package:xworkmate/runtime/desktop_platform_service.dart';
import 'package:xworkmate/runtime/go_task_service_client.dart';
import 'package:xworkmate/runtime/runtime_models.dart';
import 'package:xworkmate/runtime/secure_config_store.dart';
import 'package:xworkmate/runtime/single_agent_capabilities.dart';
import 'package:xworkmate/runtime/skill_directory_access.dart';
import 'package:xworkmate/theme/app_theme.dart';
@ -245,14 +244,6 @@ void _seedBridgeProviders(
List<SingleAgentProvider> providers,
) {
controller.bridgeAdvertisedProvidersInternal = providers;
controller.singleAgentCapabilitiesByProviderInternal = {
for (final provider in providers)
provider: SingleAgentCapabilities(
available: true,
supportedProviders: <SingleAgentProvider>[provider],
endpoint: 'bridge',
),
};
}
class _FakeSkillDirectoryAccessService implements SkillDirectoryAccessService {

View File

@ -10,7 +10,6 @@ import 'package:xworkmate/runtime/desktop_platform_service.dart';
import 'package:xworkmate/runtime/go_task_service_client.dart';
import 'package:xworkmate/runtime/runtime_models.dart';
import 'package:xworkmate/runtime/secure_config_store.dart';
import 'package:xworkmate/runtime/single_agent_capabilities.dart';
import 'package:xworkmate/runtime/skill_directory_access.dart';
import 'package:xworkmate/theme/app_theme.dart';
@ -118,14 +117,6 @@ void _seedBridgeProviders(
List<SingleAgentProvider> providers,
) {
controller.bridgeAdvertisedProvidersInternal = providers;
controller.singleAgentCapabilitiesByProviderInternal = {
for (final provider in providers)
provider: SingleAgentCapabilities(
available: true,
supportedProviders: <SingleAgentProvider>[provider],
endpoint: 'bridge',
),
};
}
class _GoldenSkillDirectoryAccessService

View File

@ -3,38 +3,6 @@ import 'package:xworkmate/runtime/runtime_models.dart';
void main() {
group('SettingsSnapshot schema v1', () {
test('defaults include provider sync presets', () {
final providerKeys = SettingsSnapshot.defaults().providerSyncDefinitions
.map((item) => item.providerKey)
.toList(growable: false);
expect(providerKeys, <String>['codex', 'opencode', 'gemini']);
});
test('round trips providerSyncDefinitions and schemaVersion', () {
final snapshot = SettingsSnapshot.defaults().copyWith(
providerSyncDefinitions: <ExternalAcpEndpointProfile>[
ExternalAcpEndpointProfile.defaultsForProvider(
SingleAgentProvider.codex,
).copyWith(endpoint: 'https://codex.example.com'),
ExternalAcpEndpointProfile.defaultsForProvider(
SingleAgentProvider.opencode,
),
ExternalAcpEndpointProfile.defaultsForProvider(
SingleAgentProvider.gemini,
),
],
);
final decoded = SettingsSnapshot.fromJson(snapshot.toJson());
expect(decoded.schemaVersion, settingsSnapshotSchemaVersion);
expect(
decoded.providerSyncDefinitions.first.endpoint,
'https://codex.example.com',
);
});
test('missing schemaVersion is rejected', () {
expect(
() => SettingsSnapshot.fromJson(<String, dynamic>{
@ -45,7 +13,34 @@ void main() {
);
});
test('removed ui restore fields are not serialized', () {
test('legacy provider sync and CLI fields are ignored on read', () {
final decoded = SettingsSnapshot.fromJson(<String, dynamic>{
'schemaVersion': settingsSnapshotSchemaVersion,
'appLanguage': 'zh',
'gatewayProfiles': <Map<String, dynamic>>[],
'providerSyncDefinitions': <Map<String, dynamic>>[
<String, dynamic>{
'providerKey': 'codex',
'label': 'Codex',
'badge': 'C',
'endpoint': 'https://codex.example.com',
'authRef': 'secret://codex',
'enabled': true,
},
],
'codexCliPath': '/tmp/codex',
});
expect(decoded.schemaVersion, settingsSnapshotSchemaVersion);
expect(
decoded.sanitizeSingleAgentProviderSelection(SingleAgentProvider.codex),
SingleAgentProvider.codex,
);
expect(decoded.toJson().containsKey('providerSyncDefinitions'), isFalse);
expect(decoded.toJson().containsKey('codexCliPath'), isFalse);
});
test('removed ui restore and local provider fields are not serialized', () {
final json = SettingsSnapshot.defaults().toJson();
expect(json.containsKey('assistantLastSessionKey'), isFalse);
@ -54,12 +49,13 @@ void main() {
expect(json.containsKey('assistantArchivedTaskKeys'), isFalse);
expect(json.containsKey('savedGatewayTargets'), isFalse);
expect(json.containsKey('externalAcpEndpoints'), isFalse);
expect(json.containsKey('providerSyncDefinitions'), isTrue);
expect(json.containsKey('providerSyncDefinitions'), isFalse);
expect(json.containsKey('codexCliPath'), isFalse);
});
});
group('AcpBridgeServerModeConfig advanced overrides', () {
test('advanced override ACP profiles are normalized to full presets', () {
test('legacy ACP bridge server profiles are ignored and not reserialized', () {
final config = AcpBridgeServerModeConfig.fromJson(<String, dynamic>{
'advancedOverrides': <String, dynamic>{
'acpBridgeServerProfiles': <Map<String, dynamic>>[
@ -67,19 +63,19 @@ void main() {
'providerKey': 'opencode',
'label': 'OpenCode',
'badge': 'O',
'endpoint': '',
'authRef': '',
'endpoint': 'https://opencode.example.com',
'authRef': 'secret://opencode',
'enabled': true,
},
],
},
});
final providerKeys = config.advancedOverrides.acpBridgeServerProfiles
.map((item) => item.providerKey)
.toList(growable: false);
final json = config.toJson();
final advancedOverrides = (json['advancedOverrides'] as Map?)?.cast<String, dynamic>();
expect(providerKeys, <String>['codex', 'opencode', 'gemini']);
expect(advancedOverrides, isNotNull);
expect(advancedOverrides!.containsKey('acpBridgeServerProfiles'), isFalse);
});
});
}