diff --git a/internal/pluginhost/adapters_executors.go b/internal/pluginhost/adapters_executors.go index 3ec4863e..38e9e383 100644 --- a/internal/pluginhost/adapters_executors.go +++ b/internal/pluginhost/adapters_executors.go @@ -347,6 +347,11 @@ func (h *Host) HasExecutorCandidateProvider(provider string) bool { return false } +// OwnsExecutor reports whether executor is an adapter managed by this host. +func (h *Host) OwnsExecutor(executor coreauth.ProviderExecutor) bool { + return h.ownsExecutor(executor) +} + func (h *Host) ownsExecutor(executor coreauth.ProviderExecutor) bool { adapter, okAdapter := executor.(*executorAdapter) return okAdapter && adapter != nil && adapter.host == h diff --git a/internal/pluginhost/adapters_test.go b/internal/pluginhost/adapters_test.go index 4512be3c..9540040f 100644 --- a/internal/pluginhost/adapters_test.go +++ b/internal/pluginhost/adapters_test.go @@ -904,6 +904,23 @@ func TestRegisterExecutorsPrunesStaleProviderAfterMigration(t *testing.T) { } } +func TestOwnsExecutorDistinguishesHostAdapters(t *testing.T) { + host := New() + owned := &executorAdapter{host: host} + foreign := &executorAdapter{host: New()} + external := &fakeProviderExecutor{provider: "provider-a"} + + if !host.OwnsExecutor(owned) { + t.Fatal("host did not recognize its executor adapter") + } + if host.OwnsExecutor(foreign) { + t.Fatal("host claimed another host's executor adapter") + } + if host.OwnsExecutor(external) { + t.Fatal("host claimed an externally owned executor") + } +} + func TestRegisterExecutorsDoesNotUnregisterStaleProviderOwnedExternally(t *testing.T) { manager := newFakeExecutorManager() exec := &fakeExecutor{identifier: "fallback-provider"} diff --git a/sdk/cliproxy/service_executor_registration_test.go b/sdk/cliproxy/service_executor_registration_test.go index 11d997d6..204e3e71 100644 --- a/sdk/cliproxy/service_executor_registration_test.go +++ b/sdk/cliproxy/service_executor_registration_test.go @@ -13,6 +13,9 @@ import ( ) type serviceTestPluginExecutor struct{} +type serviceTestSDKExecutor struct{ serviceTestPluginExecutor } + +func (serviceTestSDKExecutor) Identifier() string { return "sdk-provider" } func (serviceTestPluginExecutor) Identifier() string { return "plugin-provider" @@ -101,6 +104,32 @@ func TestRegisterAvailableExecutors(t *testing.T) { } } +func TestSyncPluginModelRuntimePreservesSDKExecutorUnlessForced(t *testing.T) { + manager := coreauth.NewManager(nil, nil, nil) + custom := serviceTestSDKExecutor{} + manager.RegisterExecutor(custom) + auth := &coreauth.Auth{ID: "private-auth", Provider: custom.Identifier()} + if _, err := manager.Register(context.Background(), auth); err != nil { + t.Fatal(err) + } + service := &Service{cfg: &config.Config{}, coreManager: manager, pluginHost: pluginhost.New()} + + service.syncPluginModelRuntime(context.Background()) + got, ok := manager.Executor(custom.Identifier()) + if !ok || got != custom { + t.Fatalf("plugin model sync replaced SDK executor with %T", got) + } + + service.registerExecutorForAuth(auth, true) + got, ok = manager.Executor(custom.Identifier()) + if !ok { + t.Fatal("forced registration removed executor") + } + if _, replaced := got.(*runtimeexecutor.OpenAICompatExecutor); !replaced { + t.Fatalf("forced registration kept %T, want *executor.OpenAICompatExecutor", got) + } +} + func TestRegisterExecutorForAuth_OpenAICompatUsesNamespacedProviderKey(t *testing.T) { testCases := []struct { name string diff --git a/sdk/cliproxy/service_executors.go b/sdk/cliproxy/service_executors.go index 6dd93ca1..1676a38e 100644 --- a/sdk/cliproxy/service_executors.go +++ b/sdk/cliproxy/service_executors.go @@ -303,10 +303,9 @@ func (s *Service) registerExecutorForAuth(a *coreauth.Auth, forceReplace bool) { return } if !forceReplace { - if existingExecutor, hasExecutor := s.coreManager.Executor(providerKey); hasExecutor { - if _, isOpenAICompatExecutor := existingExecutor.(*executor.OpenAICompatExecutor); isOpenAICompatExecutor { - return - } + if existingExecutor, hasExecutor := s.coreManager.Executor(providerKey); hasExecutor && + (s.pluginHost == nil || !s.pluginHost.OwnsExecutor(existingExecutor)) { + return } } s.coreManager.RegisterExecutor(executor.NewOpenAICompatExecutor(providerKey, cfg))