diff --git a/src/main/services/runtime/ClaudeMultimodelBridgeService.ts b/src/main/services/runtime/ClaudeMultimodelBridgeService.ts index 98b6063f..53a6bb17 100644 --- a/src/main/services/runtime/ClaudeMultimodelBridgeService.ts +++ b/src/main/services/runtime/ClaudeMultimodelBridgeService.ts @@ -1,7 +1,10 @@ import { execCli } from '@main/utils/childProcess'; import { resolveInteractiveShellEnv } from '@main/utils/shellEnv'; import { createLogger } from '@shared/utils/logger'; -import { createDefaultCliExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities'; +import { + createDefaultCliExtensionCapabilities, + createLegacyRuntimeFallbackCliExtensionCapabilities, +} from '@shared/utils/providerExtensionCapabilities'; import { resolveGeminiRuntimeAuth } from './geminiRuntimeAuth'; import { buildProviderAwareCliEnv } from './providerAwareCliEnv'; @@ -145,7 +148,7 @@ function createDefaultProviderStatus(providerId: CliProviderId): CliProviderStat capabilities: { teamLaunch: false, oneShot: false, - extensions: createDefaultCliExtensionCapabilities(), + extensions: createLegacyRuntimeFallbackCliExtensionCapabilities(), }, selectedBackendId: null, resolvedBackendId: null, @@ -159,7 +162,9 @@ function createDefaultProviderStatus(providerId: CliProviderId): CliProviderStat function mapRuntimeExtensionCapabilities( capabilities?: RuntimeExtensionCapabilitiesResponse ): CliProviderStatus['capabilities']['extensions'] { - const defaults = createDefaultCliExtensionCapabilities(); + const defaults = capabilities + ? createDefaultCliExtensionCapabilities() + : createLegacyRuntimeFallbackCliExtensionCapabilities(); return { plugins: { diff --git a/src/shared/utils/providerExtensionCapabilities.ts b/src/shared/utils/providerExtensionCapabilities.ts index c6d03b49..4d5d0c90 100644 --- a/src/shared/utils/providerExtensionCapabilities.ts +++ b/src/shared/utils/providerExtensionCapabilities.ts @@ -10,6 +10,27 @@ const SUPPORTED_SHARED_CAPABILITY: CliExtensionCapability = { reason: null, }; +const LEGACY_MULTIMODEL_FALLBACK_CAPABILITIES: CliExtensionCapabilities = { + plugins: { + status: 'unsupported', + ownership: 'shared', + reason: + 'This runtime does not declare plugin capability support. Upgrade the runtime to manage plugins here.', + }, + mcp: { + status: 'read-only', + ownership: 'shared', + reason: + 'This runtime does not declare MCP management support. Upgrade the runtime to install or remove MCP servers here.', + }, + skills: { + ...SUPPORTED_SHARED_CAPABILITY, + }, + apiKeys: { + ...SUPPORTED_SHARED_CAPABILITY, + }, +}; + export function createDefaultCliExtensionCapabilities( overrides?: Partial ): CliExtensionCapabilities { @@ -22,10 +43,22 @@ export function createDefaultCliExtensionCapabilities( }; } +export function createLegacyRuntimeFallbackCliExtensionCapabilities( + overrides?: Partial +): CliExtensionCapabilities { + return { + plugins: { ...LEGACY_MULTIMODEL_FALLBACK_CAPABILITIES.plugins }, + mcp: { ...LEGACY_MULTIMODEL_FALLBACK_CAPABILITIES.mcp }, + skills: { ...LEGACY_MULTIMODEL_FALLBACK_CAPABILITIES.skills }, + apiKeys: { ...LEGACY_MULTIMODEL_FALLBACK_CAPABILITIES.apiKeys }, + ...overrides, + }; +} + export function getCliProviderExtensionCapabilities( provider: Pick ): CliExtensionCapabilities { - return provider.capabilities.extensions ?? createDefaultCliExtensionCapabilities(); + return provider.capabilities.extensions ?? createLegacyRuntimeFallbackCliExtensionCapabilities(); } export function getCliProviderExtensionCapability( diff --git a/test/main/services/runtime/ClaudeMultimodelBridgeService.test.ts b/test/main/services/runtime/ClaudeMultimodelBridgeService.test.ts index d6cde5c7..d69ea045 100644 --- a/test/main/services/runtime/ClaudeMultimodelBridgeService.test.ts +++ b/test/main/services/runtime/ClaudeMultimodelBridgeService.test.ts @@ -254,4 +254,43 @@ describe('ClaudeMultimodelBridgeService', () => { }); expect(provider.statusMessage).toContain('ANTHROPIC_API_KEY'); }); + + it('falls back conservatively when the runtime omits extension capability metadata', async () => { + execCliMock.mockResolvedValue({ + stdout: JSON.stringify({ + providers: { + codex: { + supported: true, + authenticated: true, + verificationState: 'verified', + canLoginFromUi: true, + capabilities: { + teamLaunch: true, + oneShot: true, + }, + }, + }, + }), + stderr: '', + exitCode: 0, + }); + + const { ClaudeMultimodelBridgeService } = + await import('@main/services/runtime/ClaudeMultimodelBridgeService'); + const service = new ClaudeMultimodelBridgeService(); + + const provider = await service.getProviderStatus('/mock/agent_teams_orchestrator', 'codex'); + + expect(provider).toMatchObject({ + providerId: 'codex', + capabilities: { + extensions: { + plugins: { status: 'unsupported' }, + mcp: { status: 'read-only' }, + skills: { status: 'supported' }, + apiKeys: { status: 'supported' }, + }, + }, + }); + }); }); diff --git a/test/shared/utils/providerExtensionCapabilities.test.ts b/test/shared/utils/providerExtensionCapabilities.test.ts new file mode 100644 index 00000000..da8dd4a6 --- /dev/null +++ b/test/shared/utils/providerExtensionCapabilities.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; + +import { + createLegacyRuntimeFallbackCliExtensionCapabilities, + getCliProviderExtensionCapabilities, +} from '@shared/utils/providerExtensionCapabilities'; + +import type { CliProviderStatus } from '@shared/types'; + +function makeProvider( + overrides?: Partial +): Pick { + return { + capabilities: { + teamLaunch: false, + oneShot: false, + ...(overrides?.capabilities ?? {}), + } as CliProviderStatus['capabilities'], + }; +} + +describe('providerExtensionCapabilities', () => { + it('returns conservative fallback capabilities when runtime omits extension metadata', () => { + const capabilities = getCliProviderExtensionCapabilities( + makeProvider({ + capabilities: { + teamLaunch: true, + oneShot: true, + } as CliProviderStatus['capabilities'], + }) + ); + + expect(capabilities).toEqual(createLegacyRuntimeFallbackCliExtensionCapabilities()); + }); + + it('keeps plugins unsupported and mcp read-only in the legacy multimodel fallback', () => { + const capabilities = createLegacyRuntimeFallbackCliExtensionCapabilities(); + + expect(capabilities.plugins.status).toBe('unsupported'); + expect(capabilities.mcp.status).toBe('read-only'); + expect(capabilities.skills.status).toBe('supported'); + expect(capabilities.apiKeys.status).toBe('supported'); + }); +});