From d3909e4faf28ac8f481c679d6461f07088432f42 Mon Sep 17 00:00:00 2001 From: Brandon Hopkins Date: Sun, 26 Jul 2026 16:38:01 -0700 Subject: [PATCH] endpoint model discovery and proxy integration --- .../modules/agentnetwork/catalog/catalog.go | 35 +- .../agentnetwork/catalog/catalog_test.go | 11 + .../handlers/providers_discovery_test.go | 72 + .../handlers/providers_handler.go | 36 + .../internals/modules/agentnetwork/manager.go | 223 +++ .../agentnetwork/model_discovery_test.go | 180 ++ .../agentnetwork/types/model_discovery.go | 19 + .../modules/reverseproxy/proxy/manager.go | 6 + .../reverseproxy/proxy/manager/controller.go | 20 +- .../reverseproxy/proxy/manager_mock.go | 15 + .../shared/grpc/model_discovery_test.go | 404 +++++ management/internals/shared/grpc/proxy.go | 305 +++- .../internals/shared/grpc/proxy_test.go | 63 + proxy/internal/modeldiscovery/discovery.go | 337 ++++ .../internal/modeldiscovery/discovery_test.go | 315 ++++ proxy/management_integration_test.go | 4 + proxy/model_discovery_sync_test.go | 311 ++++ proxy/server.go | 126 +- shared/management/http/api/openapi.yml | 92 ++ shared/management/http/api/types.gen.go | 50 + shared/management/proto/proxy_service.pb.go | 1471 ++++++++++------- shared/management/proto/proxy_service.proto | 39 + 22 files changed, 3523 insertions(+), 611 deletions(-) create mode 100644 management/internals/modules/agentnetwork/handlers/providers_discovery_test.go create mode 100644 management/internals/modules/agentnetwork/model_discovery_test.go create mode 100644 management/internals/modules/agentnetwork/types/model_discovery.go create mode 100644 management/internals/shared/grpc/model_discovery_test.go create mode 100644 proxy/internal/modeldiscovery/discovery.go create mode 100644 proxy/internal/modeldiscovery/discovery_test.go create mode 100644 proxy/model_discovery_sync_test.go diff --git a/management/internals/modules/agentnetwork/catalog/catalog.go b/management/internals/modules/agentnetwork/catalog/catalog.go index 951bb3a6d..271ade08f 100644 --- a/management/internals/modules/agentnetwork/catalog/catalog.go +++ b/management/internals/modules/agentnetwork/catalog/catalog.go @@ -101,6 +101,17 @@ type Provider struct { // upstream provider + credentials on Portkey's hosted side). ExtraHeaders []ExtraHeader Models []Model + // ModelDiscovery configures provider-specific discovery behavior. A nil + // profile means discovery is unsupported. The profile never carries + // caller-supplied paths: the proxy owns the fixed endpoint allowlist. + ModelDiscovery *ModelDiscovery +} + +// ModelDiscovery describes the safe, catalog-owned fallback behavior for a +// discoverable provider. Every discovery starts with OpenAI-compatible +// /v1/models; OllamaFallback permits /api/tags only when that route is absent. +type ModelDiscovery struct { + OllamaFallback bool } // ExtraHeader names a single optional per-provider routing/config @@ -732,6 +743,9 @@ var providers = []Provider{ DefaultContentType: "application/json", BrandColor: "#000000", Models: []Model{}, + ModelDiscovery: &ModelDiscovery{ + OllamaFallback: true, + }, }, { ID: "custom", @@ -815,16 +829,17 @@ func (p Provider) ToAPIResponse() api.AgentNetworkCatalogProvider { kind = api.AgentNetworkCatalogProviderKindCustom } resp := api.AgentNetworkCatalogProvider{ - Id: p.ID, - Name: p.Name, - Description: p.Description, - DefaultHost: p.DefaultHost, - Kind: kind, - AuthMode: api.AgentNetworkCatalogProviderAuthMode(p.EffectiveAuthMode()), - AuthHeaderTemplate: p.AuthHeaderTemplate, - DefaultContentType: p.DefaultContentType, - BrandColor: p.BrandColor, - Models: models, + Id: p.ID, + Name: p.Name, + Description: p.Description, + DefaultHost: p.DefaultHost, + Kind: kind, + AuthMode: api.AgentNetworkCatalogProviderAuthMode(p.EffectiveAuthMode()), + AuthHeaderTemplate: p.AuthHeaderTemplate, + DefaultContentType: p.DefaultContentType, + BrandColor: p.BrandColor, + Models: models, + SupportsModelDiscovery: p.ModelDiscovery != nil, } if len(p.ExtraHeaders) > 0 { extras := make([]api.AgentNetworkCatalogExtraHeader, 0, len(p.ExtraHeaders)) diff --git a/management/internals/modules/agentnetwork/catalog/catalog_test.go b/management/internals/modules/agentnetwork/catalog/catalog_test.go index c1020e05b..b2cd5a8da 100644 --- a/management/internals/modules/agentnetwork/catalog/catalog_test.go +++ b/management/internals/modules/agentnetwork/catalog/catalog_test.go @@ -23,15 +23,26 @@ func TestOllamaCatalogEntry(t *testing.T) { assert.Equal(t, "application/json", entry.DefaultContentType) assert.Empty(t, entry.ParserID, "Ollama preserves the untagged vLLM/custom routing behavior") assert.Empty(t, entry.Models, "Ollama models are installed dynamically on the configured endpoint") + require.NotNil(t, entry.ModelDiscovery) + assert.True(t, entry.ModelDiscovery.OllamaFallback) wire := entry.ToAPIResponse() assert.Equal(t, "ollama", wire.Id) assert.Equal(t, api.AgentNetworkCatalogProviderKindCustom, wire.Kind) assert.Equal(t, api.AgentNetworkCatalogProviderAuthModeOptional, wire.AuthMode) + assert.True(t, wire.SupportsModelDiscovery) assert.NotNil(t, wire.Models) assert.Empty(t, wire.Models) } +func TestOnlyOllamaSupportsModelDiscovery(t *testing.T) { + for _, entry := range All() { + supportsDiscovery := entry.ModelDiscovery != nil + assert.Equal(t, entry.ID == "ollama", supportsDiscovery, entry.ID) + assert.Equal(t, supportsDiscovery, entry.ToAPIResponse().SupportsModelDiscovery, entry.ID) + } +} + func TestCatalogAuthenticationModes(t *testing.T) { openAI, ok := Lookup("openai_api") require.True(t, ok) diff --git a/management/internals/modules/agentnetwork/handlers/providers_discovery_test.go b/management/internals/modules/agentnetwork/handlers/providers_discovery_test.go new file mode 100644 index 000000000..a815563f2 --- /dev/null +++ b/management/internals/modules/agentnetwork/handlers/providers_discovery_test.go @@ -0,0 +1,72 @@ +package handlers + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/internals/modules/agentnetwork" + agentNetworkTypes "github.com/netbirdio/netbird/management/internals/modules/agentnetwork/types" + nbcontext "github.com/netbirdio/netbird/management/server/context" + "github.com/netbirdio/netbird/shared/auth" + "github.com/netbirdio/netbird/shared/management/http/api" +) + +type discoveryManagerStub struct { + agentnetwork.Manager + result *agentNetworkTypes.ModelDiscoveryResult + err error + accountID string + userID string + providerID string +} + +func (m *discoveryManagerStub) DiscoverProviderModels(_ context.Context, accountID, userID, providerID string) (*agentNetworkTypes.ModelDiscoveryResult, error) { + m.accountID = accountID + m.userID = userID + m.providerID = providerID + return m.result, m.err +} + +func TestDiscoverProviderModelsHandler(t *testing.T) { + manager := &discoveryManagerStub{ + Manager: agentnetwork.NewManagerMock(), + result: &agentNetworkTypes.ModelDiscoveryResult{ + RequestID: "probe-123", + Source: "ollama_api_tags", + ProxyCluster: "private.example.com", + Models: []agentNetworkTypes.DiscoveredModel{ + {ID: "llama3.2:latest", Label: "llama3.2:latest"}, + }, + }, + } + router := mux.NewRouter() + RegisterEndpoints(manager, router) + + req := httptest.NewRequest(http.MethodPost, "/agent-network/providers/provider-1/discover-models", nil) + req = nbcontext.SetUserAuthInRequest(req, auth.UserAuth{ + UserId: testUserID, + AccountId: testAccountID, + }) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + + require.Equal(t, http.StatusOK, rec.Code, rec.Body.String()) + assert.Equal(t, "no-store", rec.Header().Get("Cache-Control")) + var response api.AgentNetworkModelDiscoveryResponse + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &response)) + assert.Equal(t, testAccountID, manager.accountID) + assert.Equal(t, testUserID, manager.userID) + assert.Equal(t, "provider-1", manager.providerID) + assert.Equal(t, "probe-123", response.RequestId) + assert.Equal(t, "private.example.com", response.ProxyCluster) + assert.Equal(t, api.AgentNetworkModelDiscoveryResponseSourceOllamaApiTags, response.Source) + require.Len(t, response.Models, 1) + assert.Equal(t, "llama3.2:latest", response.Models[0].Id) +} diff --git a/management/internals/modules/agentnetwork/handlers/providers_handler.go b/management/internals/modules/agentnetwork/handlers/providers_handler.go index f4ee45679..cb501426d 100644 --- a/management/internals/modules/agentnetwork/handlers/providers_handler.go +++ b/management/internals/modules/agentnetwork/handlers/providers_handler.go @@ -35,6 +35,7 @@ func RegisterEndpoints(manager agentnetwork.Manager, router *mux.Router) { router.HandleFunc("/agent-network/providers/{providerId}", h.getProvider).Methods("GET", "OPTIONS") router.HandleFunc("/agent-network/providers/{providerId}", h.updateProvider).Methods("PUT", "OPTIONS") router.HandleFunc("/agent-network/providers/{providerId}", h.deleteProvider).Methods("DELETE", "OPTIONS") + router.HandleFunc("/agent-network/providers/{providerId}/discover-models", h.discoverProviderModels).Methods("POST", "OPTIONS") h.addPolicyEndpoints(router) h.addGuardrailEndpoints(router) h.addSettingsEndpoints(router) @@ -98,6 +99,41 @@ func (h *handler) getProvider(w http.ResponseWriter, r *http.Request) { util.WriteJSONObject(r.Context(), w, provider.ToAPIResponse()) } +func (h *handler) discoverProviderModels(w http.ResponseWriter, r *http.Request) { + userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + providerID := strings.TrimSpace(mux.Vars(r)["providerId"]) + if providerID == "" { + util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "provider ID is required"), w) + return + } + + result, err := h.manager.DiscoverProviderModels(r.Context(), userAuth.AccountId, userAuth.UserId, providerID) + if err != nil { + util.WriteError(r.Context(), err, w) + return + } + + models := make([]api.AgentNetworkDiscoveredModel, 0, len(result.Models)) + for _, model := range result.Models { + models = append(models, api.AgentNetworkDiscoveredModel{ + Id: model.ID, + Label: model.Label, + }) + } + w.Header().Set("Cache-Control", "no-store") + util.WriteJSONObject(r.Context(), w, api.AgentNetworkModelDiscoveryResponse{ + Models: models, + Source: api.AgentNetworkModelDiscoveryResponseSource(result.Source), + ProxyCluster: result.ProxyCluster, + RequestId: result.RequestID, + }) +} + func (h *handler) createProvider(w http.ResponseWriter, r *http.Request) { userAuth, err := nbcontext.GetUserAuthFromContext(r.Context()) if err != nil { diff --git a/management/internals/modules/agentnetwork/manager.go b/management/internals/modules/agentnetwork/manager.go index 2bcad7b8c..6165d0685 100644 --- a/management/internals/modules/agentnetwork/manager.go +++ b/management/internals/modules/agentnetwork/manager.go @@ -9,6 +9,8 @@ import ( "strings" "sync" "time" + "unicode" + "unicode/utf8" log "github.com/sirupsen/logrus" @@ -49,6 +51,7 @@ func ensureSessionKeys(p *types.Provider) error { type Manager interface { GetAllProviders(ctx context.Context, accountID, userID string) ([]*types.Provider, error) GetProvider(ctx context.Context, accountID, userID, providerID string) (*types.Provider, error) + DiscoverProviderModels(ctx context.Context, accountID, userID, providerID string) (*types.ModelDiscoveryResult, error) CreateProvider(ctx context.Context, userID string, provider *types.Provider, bootstrapCluster string) (*types.Provider, error) UpdateProvider(ctx context.Context, userID string, provider *types.Provider) (*types.Provider, error) DeleteProvider(ctx context.Context, accountID, userID, providerID string) error @@ -129,8 +132,27 @@ type managerImpl struct { // state; concurrent provider creates would otherwise race. labelRngMu sync.Mutex labelRng *rand.Rand + + // discoveryAttempts provides lightweight per-provider admission control for + // the explicit endpoint probe. It prevents duplicate clicks from occupying + // multiple proxy control-stream requests at once and adds a short cooldown + // after each attempt. + discoveryMu sync.Mutex + discoveryAttempts map[string]modelDiscoveryAttempt } +type modelDiscoveryAttempt struct { + inFlight bool + lastStarted time.Time +} + +const ( + modelDiscoveryTimeout = 10 * time.Second + modelDiscoveryCooldown = 2 * time.Second + maxDiscoveredModels = 500 + maxDiscoveredModelLen = 512 +) + // NewManager constructs the persistent Agent Network manager. The // manager persists provider/policy/guardrail configuration and, on // every mutation, reconciles the in-memory synthesised reverse-proxy @@ -149,6 +171,7 @@ func NewManager( proxyController: proxyController, reconcileCache: make(map[string]map[string]*proto.ProxyMapping), labelRng: rand.New(rand.NewSource(time.Now().UnixNano())), + discoveryAttempts: make(map[string]modelDiscoveryAttempt), } } @@ -166,6 +189,202 @@ func (m *managerImpl) GetProvider(ctx context.Context, accountID, userID, provid return m.store.GetAgentNetworkProviderByID(ctx, store.LockingStrengthNone, accountID, providerID) } +// DiscoverProviderModels asks a capable proxy in the account's selected +// cluster to query the persisted provider endpoint. The browser supplies only +// the provider id: URL, TLS policy, and credential are loaded here so a caller +// cannot turn this operation into an arbitrary network probe. +func (m *managerImpl) DiscoverProviderModels(ctx context.Context, accountID, userID, providerID string) (*types.ModelDiscoveryResult, error) { + if err := m.requirePermission(ctx, accountID, userID, operations.Update); err != nil { + return nil, err + } + providerID = strings.TrimSpace(providerID) + if providerID == "" { + return nil, status.Errorf(status.InvalidArgument, "provider ID is required") + } + + provider, err := m.store.GetAgentNetworkProviderByID(ctx, store.LockingStrengthNone, accountID, providerID) + if err != nil { + return nil, err + } + entry, ok := catalog.Lookup(provider.ProviderID) + if !ok { + return nil, status.Errorf(status.PreconditionFailed, "provider references an unknown catalog provider") + } + if entry.ModelDiscovery == nil { + return nil, status.Errorf(status.PreconditionFailed, "provider type %q does not support model discovery", provider.ProviderID) + } + if m.proxyController == nil { + return nil, status.Errorf(status.PreconditionFailed, "model discovery is unavailable") + } + + settings, err := m.store.GetAgentNetworkSettings(ctx, store.LockingStrengthNone, accountID) + if err != nil { + var statusErr *status.Error + if errors.As(err, &statusErr) && statusErr.Type() == status.NotFound { + return nil, status.Errorf(status.PreconditionFailed, "configure an Agent Network proxy cluster before discovering models") + } + return nil, err + } + cluster := strings.TrimSpace(settings.Cluster) + if cluster == "" { + return nil, status.Errorf(status.PreconditionFailed, "configure an Agent Network proxy cluster before discovering models") + } + + authHeaderName, authHeaderValue, gcpKey, err := providerAuthHeader(provider) + if err != nil { + return nil, status.Errorf(status.PreconditionFailed, "provider authentication is not configured correctly") + } + if gcpKey != "" { + return nil, status.Errorf(status.PreconditionFailed, "provider authentication is not supported for model discovery") + } + + attemptKey := accountID + "\x00" + provider.ID + if !m.beginModelDiscovery(attemptKey, time.Now()) { + return nil, status.Errorf(status.TooManyRequests, "model discovery is already running or was requested too recently") + } + defer m.finishModelDiscovery(attemptKey) + + probeCtx, cancel := context.WithTimeout(ctx, modelDiscoveryTimeout) + defer cancel() + probeResult, err := m.proxyController.DiscoverModels(probeCtx, accountID, cluster, &proto.ModelDiscoveryRequest{ + UpstreamUrl: strings.TrimSpace(provider.UpstreamURL), + AuthHeaderName: authHeaderName, + AuthHeaderValue: authHeaderValue, + SkipTlsVerify: provider.SkipTLSVerification, + OllamaFallback: entry.ModelDiscovery.OllamaFallback, + }) + if err != nil { + if errors.Is(err, context.DeadlineExceeded) || errors.Is(probeCtx.Err(), context.DeadlineExceeded) { + return nil, status.Errorf(status.PreconditionFailed, "model discovery timed out") + } + if _, ok := status.FromError(err); ok { + return nil, err + } + return nil, status.Errorf(status.PreconditionFailed, "model discovery could not be completed") + } + if probeResult == nil { + return nil, status.Errorf(status.PreconditionFailed, "proxy returned an empty model discovery response") + } + if strings.TrimSpace(probeResult.Error) != "" { + message := safeModelDiscoveryText(probeResult.Error, 256) + if message == "" { + message = "proxy reported a discovery failure" + } + requestID := safeModelDiscoveryText(probeResult.RequestId, 128) + if requestID != "" { + return nil, status.Errorf(status.PreconditionFailed, "model discovery failed: %s (request_id: %s)", message, requestID) + } + return nil, status.Errorf(status.PreconditionFailed, "model discovery failed: %s", message) + } + + requestID := safeModelDiscoveryText(probeResult.RequestId, 128) + if requestID == "" { + return nil, status.Errorf(status.PreconditionFailed, "proxy returned an uncorrelated model discovery response") + } + source := strings.TrimSpace(probeResult.Source) + switch source { + case "openai_v1_models", "ollama_api_tags": + default: + return nil, status.Errorf(status.PreconditionFailed, "proxy returned an unsupported model discovery response") + } + + return &types.ModelDiscoveryResult{ + RequestID: requestID, + Source: source, + ProxyCluster: cluster, + Models: normalizeDiscoveredModels(probeResult.Models), + }, nil +} + +func (m *managerImpl) beginModelDiscovery(key string, now time.Time) bool { + m.discoveryMu.Lock() + defer m.discoveryMu.Unlock() + if m.discoveryAttempts == nil { + m.discoveryAttempts = make(map[string]modelDiscoveryAttempt) + } + attempt := m.discoveryAttempts[key] + if attempt.inFlight || (!attempt.lastStarted.IsZero() && now.Sub(attempt.lastStarted) < modelDiscoveryCooldown) { + return false + } + attempt.inFlight = true + attempt.lastStarted = now + m.discoveryAttempts[key] = attempt + return true +} + +func (m *managerImpl) finishModelDiscovery(key string) { + m.discoveryMu.Lock() + attempt, ok := m.discoveryAttempts[key] + if !ok { + m.discoveryMu.Unlock() + return + } + attempt.inFlight = false + m.discoveryAttempts[key] = attempt + m.discoveryMu.Unlock() + + remaining := time.Until(attempt.lastStarted.Add(modelDiscoveryCooldown)) + if remaining <= 0 { + m.expireModelDiscoveryAttempt(key, attempt.lastStarted) + return + } + time.AfterFunc(remaining, func() { + m.expireModelDiscoveryAttempt(key, attempt.lastStarted) + }) +} + +func (m *managerImpl) expireModelDiscoveryAttempt(key string, lastStarted time.Time) { + m.discoveryMu.Lock() + defer m.discoveryMu.Unlock() + attempt, ok := m.discoveryAttempts[key] + if ok && !attempt.inFlight && attempt.lastStarted.Equal(lastStarted) { + delete(m.discoveryAttempts, key) + } +} + +func normalizeDiscoveredModels(models []*proto.ModelDiscoveryModel) []types.DiscoveredModel { + out := make([]types.DiscoveredModel, 0, min(len(models), maxDiscoveredModels)) + seen := make(map[string]struct{}, min(len(models), maxDiscoveredModels)) + for _, model := range models { + if model == nil { + continue + } + id := safeModelDiscoveryText(model.Id, maxDiscoveredModelLen) + if id == "" || len(id) > maxDiscoveredModelLen { + continue + } + if _, ok := seen[id]; ok { + continue + } + label := safeModelDiscoveryText(model.Label, maxDiscoveredModelLen) + if label == "" || len(label) > maxDiscoveredModelLen { + label = id + } + seen[id] = struct{}{} + out = append(out, types.DiscoveredModel{ID: id, Label: label}) + if len(out) == maxDiscoveredModels { + break + } + } + slices.SortFunc(out, func(a, b types.DiscoveredModel) int { + return strings.Compare(a.ID, b.ID) + }) + return out +} + +func safeModelDiscoveryText(value string, maxLen int) string { + value = strings.TrimSpace(value) + if value == "" || len(value) > maxLen || !utf8.ValidString(value) { + return "" + } + for _, r := range value { + if unicode.IsControl(r) { + return "" + } + } + return value +} + // CreateProvider persists a new provider for the account. bootstrapCluster // is used only when the per-account agent-network Settings row hasn't // been created yet; otherwise it is ignored (the cluster is pinned on @@ -847,6 +1066,10 @@ func (*mockManager) GetProvider(_ context.Context, _, _, _ string) (*types.Provi return &types.Provider{}, nil } +func (*mockManager) DiscoverProviderModels(_ context.Context, _, _, _ string) (*types.ModelDiscoveryResult, error) { + return nil, status.Errorf(status.PreconditionFailed, "model discovery is unavailable") +} + func (*mockManager) CreateProvider(_ context.Context, _ string, p *types.Provider, _ string) (*types.Provider, error) { return p, nil } diff --git a/management/internals/modules/agentnetwork/model_discovery_test.go b/management/internals/modules/agentnetwork/model_discovery_test.go new file mode 100644 index 000000000..274614c03 --- /dev/null +++ b/management/internals/modules/agentnetwork/model_discovery_test.go @@ -0,0 +1,180 @@ +package agentnetwork + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/management/internals/modules/agentnetwork/types" + "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy" + "github.com/netbirdio/netbird/management/server/permissions" + "github.com/netbirdio/netbird/management/server/permissions/modules" + "github.com/netbirdio/netbird/management/server/permissions/operations" + "github.com/netbirdio/netbird/management/server/store" + "github.com/netbirdio/netbird/shared/management/proto" + "github.com/netbirdio/netbird/shared/management/status" +) + +func newModelDiscoveryManager(t *testing.T) (*managerImpl, *store.MockStore, *permissions.MockManager, *proxy.MockController) { + t.Helper() + ctrl := gomock.NewController(t) + mockStore := store.NewMockStore(ctrl) + mockPermissions := permissions.NewMockManager(ctrl) + mockProxy := proxy.NewMockController(ctrl) + return &managerImpl{ + store: mockStore, + permissionsManager: mockPermissions, + proxyController: mockProxy, + discoveryAttempts: make(map[string]modelDiscoveryAttempt), + }, mockStore, mockPermissions, mockProxy +} + +func allowModelDiscovery(mockPermissions *permissions.MockManager) { + mockPermissions.EXPECT(). + ValidateUserPermissions(gomock.Any(), "account-1", "user-1", modules.AgentNetwork, operations.Update). + Return(true, context.Background(), nil) +} + +func TestDiscoverProviderModelsUsesPersistedProviderAndCluster(t *testing.T) { + manager, mockStore, mockPermissions, mockProxy := newModelDiscoveryManager(t) + allowModelDiscovery(mockPermissions) + + provider := &types.Provider{ + ID: "provider-1", + AccountID: "account-1", + ProviderID: "ollama", + UpstreamURL: "http://ollama.internal:11434/base", + APIKey: "stored-key", + SkipTLSVerification: true, + } + mockStore.EXPECT(). + GetAgentNetworkProviderByID(gomock.Any(), store.LockingStrengthNone, "account-1", "provider-1"). + Return(provider, nil) + mockStore.EXPECT(). + GetAgentNetworkSettings(gomock.Any(), store.LockingStrengthNone, "account-1"). + Return(&types.Settings{AccountID: "account-1", Cluster: "private.example.com"}, nil) + mockProxy.EXPECT(). + DiscoverModels(gomock.Any(), "account-1", "private.example.com", gomock.Any()). + DoAndReturn(func(_ context.Context, _, _ string, request *proto.ModelDiscoveryRequest) (*proto.ModelDiscoveryResult, error) { + assert.Empty(t, request.RequestId, "the control-plane server owns request IDs") + assert.Equal(t, provider.UpstreamURL, request.UpstreamUrl) + assert.Equal(t, "Authorization", request.AuthHeaderName) + assert.Equal(t, "Bearer stored-key", request.AuthHeaderValue) + assert.True(t, request.SkipTlsVerify) + assert.True(t, request.OllamaFallback) + return &proto.ModelDiscoveryResult{ + RequestId: "probe-123", + Source: "openai_v1_models", + Models: []*proto.ModelDiscoveryModel{ + {Id: "zeta", Label: ""}, + {Id: "alpha", Label: "Alpha"}, + {Id: "zeta", Label: "duplicate"}, + {Id: "bad\x00model", Label: "bad"}, + {Id: strings.Repeat("x", maxDiscoveredModelLen+1), Label: "too long"}, + }, + }, nil + }) + + result, err := manager.DiscoverProviderModels(context.Background(), "account-1", "user-1", "provider-1") + require.NoError(t, err) + assert.Equal(t, "probe-123", result.RequestID) + assert.Equal(t, "openai_v1_models", result.Source) + assert.Equal(t, "private.example.com", result.ProxyCluster) + assert.Equal(t, []types.DiscoveredModel{ + {ID: "alpha", Label: "Alpha"}, + {ID: "zeta", Label: "zeta"}, + }, result.Models) +} + +func TestDiscoverProviderModelsRejectsUnsupportedProvider(t *testing.T) { + manager, mockStore, mockPermissions, _ := newModelDiscoveryManager(t) + allowModelDiscovery(mockPermissions) + mockStore.EXPECT(). + GetAgentNetworkProviderByID(gomock.Any(), store.LockingStrengthNone, "account-1", "provider-1"). + Return(&types.Provider{ + ID: "provider-1", + AccountID: "account-1", + ProviderID: "openai_api", + }, nil) + + _, err := manager.DiscoverProviderModels(context.Background(), "account-1", "user-1", "provider-1") + require.Error(t, err) + statusErr, ok := status.FromError(err) + require.True(t, ok) + assert.Equal(t, status.PreconditionFailed, statusErr.Type()) + assert.Contains(t, err.Error(), "does not support") +} + +func TestDiscoverProviderModelsPreservesCorrelatedProxyError(t *testing.T) { + manager, mockStore, mockPermissions, mockProxy := newModelDiscoveryManager(t) + allowModelDiscovery(mockPermissions) + mockStore.EXPECT(). + GetAgentNetworkProviderByID(gomock.Any(), store.LockingStrengthNone, "account-1", "provider-1"). + Return(&types.Provider{ + ID: "provider-1", + AccountID: "account-1", + ProviderID: "ollama", + UpstreamURL: "http://ollama.internal:11434", + }, nil) + mockStore.EXPECT(). + GetAgentNetworkSettings(gomock.Any(), store.LockingStrengthNone, "account-1"). + Return(&types.Settings{AccountID: "account-1", Cluster: "private.example.com"}, nil) + mockProxy.EXPECT(). + DiscoverModels(gomock.Any(), "account-1", "private.example.com", gomock.Any()). + Return(&proto.ModelDiscoveryResult{ + RequestId: "probe-failed", + Error: "upstream returned HTTP 401", + }, nil) + + _, err := manager.DiscoverProviderModels(context.Background(), "account-1", "user-1", "provider-1") + require.Error(t, err) + assert.Contains(t, err.Error(), "upstream returned HTTP 401") + assert.Contains(t, err.Error(), "probe-failed") +} + +func TestModelDiscoveryAdmissionControl(t *testing.T) { + manager := &managerImpl{} + start := time.Now() + require.True(t, manager.beginModelDiscovery("account/provider", start)) + assert.False(t, manager.beginModelDiscovery("account/provider", start.Add(time.Second))) + + manager.finishModelDiscovery("account/provider") + assert.False(t, manager.beginModelDiscovery("account/provider", start.Add(time.Second))) + assert.True(t, manager.beginModelDiscovery("account/provider", start.Add(modelDiscoveryCooldown))) +} + +func TestModelDiscoveryAdmissionControlExpiresFinishedAttempts(t *testing.T) { + manager := &managerImpl{} + start := time.Now() + require.True(t, manager.beginModelDiscovery("account/provider", start)) + manager.finishModelDiscovery("account/provider") + + manager.expireModelDiscoveryAttempt("account/provider", start) + + manager.discoveryMu.Lock() + _, retained := manager.discoveryAttempts["account/provider"] + manager.discoveryMu.Unlock() + assert.False(t, retained) +} + +func TestModelDiscoveryAdmissionControlKeepsReplacementAttempt(t *testing.T) { + manager := &managerImpl{} + start := time.Now() + require.True(t, manager.beginModelDiscovery("account/provider", start)) + manager.finishModelDiscovery("account/provider") + require.True(t, manager.beginModelDiscovery("account/provider", start.Add(modelDiscoveryCooldown))) + + manager.expireModelDiscoveryAttempt("account/provider", start) + + manager.discoveryMu.Lock() + attempt, retained := manager.discoveryAttempts["account/provider"] + manager.discoveryMu.Unlock() + require.True(t, retained) + assert.True(t, attempt.inFlight) + assert.Equal(t, start.Add(modelDiscoveryCooldown), attempt.lastStarted) +} diff --git a/management/internals/modules/agentnetwork/types/model_discovery.go b/management/internals/modules/agentnetwork/types/model_discovery.go new file mode 100644 index 000000000..5054e42c7 --- /dev/null +++ b/management/internals/modules/agentnetwork/types/model_discovery.go @@ -0,0 +1,19 @@ +package types + +// DiscoveredModel is a normalized model identifier returned by a provider's +// persisted upstream endpoint. Discovery does not persist models; the caller +// must explicitly update the provider to opt into the returned list. +type DiscoveredModel struct { + ID string + Label string +} + +// ModelDiscoveryResult describes a successful proxy-executed discovery probe. +// RequestID correlates the management request with the proxy control message, +// and ProxyCluster identifies the network vantage point that ran it. +type ModelDiscoveryResult struct { + RequestID string + Source string + ProxyCluster string + Models []DiscoveredModel +} diff --git a/management/internals/modules/reverseproxy/proxy/manager.go b/management/internals/modules/reverseproxy/proxy/manager.go index 22f1007ec..29d079e5d 100644 --- a/management/internals/modules/reverseproxy/proxy/manager.go +++ b/management/internals/modules/reverseproxy/proxy/manager.go @@ -4,11 +4,16 @@ package proxy import ( "context" + "errors" "time" "github.com/netbirdio/netbird/shared/management/proto" ) +// ErrModelDiscoveryUnavailable is returned when no connected proxy in the +// requested cluster can execute a model-discovery request. +var ErrModelDiscoveryUnavailable = errors.New("model discovery proxy unavailable") + // Manager defines the interface for proxy operations type Manager interface { Connect(ctx context.Context, proxyID, sessionID, clusterAddress, ipAddress string, accountID *string, capabilities *Capabilities) (*Proxy, error) @@ -38,6 +43,7 @@ type OIDCValidationConfig struct { // Controller is responsible for managing proxy clusters and routing service updates. type Controller interface { SendServiceUpdateToCluster(ctx context.Context, accountID string, update *proto.ProxyMapping, clusterAddr string) + DiscoverModels(ctx context.Context, accountID, clusterAddr string, req *proto.ModelDiscoveryRequest) (*proto.ModelDiscoveryResult, error) GetOIDCValidationConfig() OIDCValidationConfig RegisterProxyToCluster(ctx context.Context, clusterAddr, proxyID string) error UnregisterProxyFromCluster(ctx context.Context, clusterAddr, proxyID string) error diff --git a/management/internals/modules/reverseproxy/proxy/manager/controller.go b/management/internals/modules/reverseproxy/proxy/manager/controller.go index e5b3e9886..266bd64be 100644 --- a/management/internals/modules/reverseproxy/proxy/manager/controller.go +++ b/management/internals/modules/reverseproxy/proxy/manager/controller.go @@ -39,6 +39,12 @@ func (c *GRPCController) SendServiceUpdateToCluster(ctx context.Context, account c.metrics.IncrementServiceUpdateSendCount(clusterAddr) } +// DiscoverModels executes a correlated model-discovery request on one capable +// proxy connected to the requested cluster. +func (c *GRPCController) DiscoverModels(ctx context.Context, accountID, clusterAddr string, req *proto.ModelDiscoveryRequest) (*proto.ModelDiscoveryResult, error) { + return c.proxyGRPCServer.DiscoverModels(ctx, accountID, clusterAddr, req) +} + // GetOIDCValidationConfig returns the OIDC validation configuration from the gRPC server. func (c *GRPCController) GetOIDCValidationConfig() proxy.OIDCValidationConfig { return c.proxyGRPCServer.GetOIDCValidationConfig() @@ -50,10 +56,12 @@ func (c *GRPCController) RegisterProxyToCluster(ctx context.Context, clusterAddr return nil } proxySet, _ := c.clusterProxies.LoadOrStore(clusterAddr, &sync.Map{}) - proxySet.(*sync.Map).Store(proxyID, struct{}{}) + _, alreadyRegistered := proxySet.(*sync.Map).LoadOrStore(proxyID, struct{}{}) log.WithContext(ctx).Debugf("Registered proxy %s to cluster %s", proxyID, clusterAddr) - c.metrics.IncrementProxyConnectionCount(clusterAddr) + if !alreadyRegistered { + c.metrics.IncrementProxyConnectionCount(clusterAddr) + } return nil } @@ -64,10 +72,10 @@ func (c *GRPCController) UnregisterProxyFromCluster(ctx context.Context, cluster return nil } if proxySet, ok := c.clusterProxies.Load(clusterAddr); ok { - proxySet.(*sync.Map).Delete(proxyID) - log.WithContext(ctx).Debugf("Unregistered proxy %s from cluster %s", proxyID, clusterAddr) - - c.metrics.DecrementProxyConnectionCount(clusterAddr) + if _, registered := proxySet.(*sync.Map).LoadAndDelete(proxyID); registered { + log.WithContext(ctx).Debugf("Unregistered proxy %s from cluster %s", proxyID, clusterAddr) + c.metrics.DecrementProxyConnectionCount(clusterAddr) + } } return nil } diff --git a/management/internals/modules/reverseproxy/proxy/manager_mock.go b/management/internals/modules/reverseproxy/proxy/manager_mock.go index d2be46c9f..f8d08f04f 100644 --- a/management/internals/modules/reverseproxy/proxy/manager_mock.go +++ b/management/internals/modules/reverseproxy/proxy/manager_mock.go @@ -259,6 +259,21 @@ func (m *MockController) EXPECT() *MockControllerMockRecorder { return m.recorder } +// DiscoverModels mocks base method. +func (m *MockController) DiscoverModels(ctx context.Context, accountID, clusterAddr string, req *proto.ModelDiscoveryRequest) (*proto.ModelDiscoveryResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DiscoverModels", ctx, accountID, clusterAddr, req) + ret0, _ := ret[0].(*proto.ModelDiscoveryResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DiscoverModels indicates an expected call of DiscoverModels. +func (mr *MockControllerMockRecorder) DiscoverModels(ctx, accountID, clusterAddr, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DiscoverModels", reflect.TypeOf((*MockController)(nil).DiscoverModels), ctx, accountID, clusterAddr, req) +} + // GetOIDCValidationConfig mocks base method. func (m *MockController) GetOIDCValidationConfig() OIDCValidationConfig { m.ctrl.T.Helper() diff --git a/management/internals/shared/grpc/model_discovery_test.go b/management/internals/shared/grpc/model_discovery_test.go new file mode 100644 index 000000000..ed172c34c --- /dev/null +++ b/management/internals/shared/grpc/model_discovery_test.go @@ -0,0 +1,404 @@ +package grpc + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + rpproxy "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy" + "github.com/netbirdio/netbird/shared/management/proto" + nbstatus "github.com/netbirdio/netbird/shared/management/status" +) + +type modelDiscoveryCall struct { + result *proto.ModelDiscoveryResult + err error +} + +func newModelDiscoveryTestServer(t *testing.T) (*ProxyServiceServer, *testProxyController) { + t.Helper() + controller := newTestProxyController() + server := &ProxyServiceServer{ + proxyController: controller, + modelDiscoveryPending: make(map[string]*pendingModelDiscovery), + } + return server, controller +} + +func addModelDiscoveryTestConnection( + t *testing.T, + server *ProxyServiceServer, + controller *testProxyController, + proxyID, sessionID, cluster string, + accountID *string, + supportsDiscovery bool, +) (*proxyConnection, context.CancelFunc) { + t.Helper() + ctx, cancel := context.WithCancel(context.Background()) + ready := make(chan struct{}) + close(ready) + conn := &proxyConnection{ + proxyID: proxyID, + sessionID: sessionID, + address: cluster, + accountID: accountID, + capabilities: &proto.ProxyCapabilities{ + SupportsModelDiscovery: &supportsDiscovery, + }, + syncStream: &syncRecordingStream{}, + modelDiscoveryChan: make(chan *proto.ModelDiscoveryRequest, modelDiscoveryQueueSize), + ready: ready, + ctx: ctx, + cancel: cancel, + } + server.connectedProxies.Store(proxyID, conn) + require.NoError(t, controller.RegisterProxyToCluster(context.Background(), cluster, proxyID)) + t.Cleanup(func() { + cancel() + server.connectedProxies.Delete(proxyID) + _ = controller.UnregisterProxyFromCluster(context.Background(), cluster, proxyID) + }) + return conn, cancel +} + +func callDiscoverModels( + server *ProxyServiceServer, + ctx context.Context, + accountID, cluster string, + req *proto.ModelDiscoveryRequest, +) <-chan modelDiscoveryCall { + done := make(chan modelDiscoveryCall, 1) + go func() { + result, err := server.DiscoverModels(ctx, accountID, cluster, req) + done <- modelDiscoveryCall{result: result, err: err} + }() + return done +} + +func TestDiscoverModels_CorrelatesResultAndCopiesSensitiveRequest(t *testing.T) { + server, controller := newModelDiscoveryTestServer(t) + conn, _ := addModelDiscoveryTestConnection( + t, server, controller, "proxy-a", "session-a", "cluster.example.com", nil, true, + ) + + callerReq := &proto.ModelDiscoveryRequest{ + RequestId: "caller-controlled", + UpstreamUrl: "http://ollama.internal:11434", + AuthHeaderName: "Authorization", + AuthHeaderValue: "Bearer secret", + SkipTlsVerify: true, + OllamaFallback: true, + } + done := callDiscoverModels(server, context.Background(), "account-a", "cluster.example.com", callerReq) + + wireReq := <-conn.modelDiscoveryChan + assert.NotEmpty(t, wireReq.GetRequestId()) + assert.NotEqual(t, callerReq.GetRequestId(), wireReq.GetRequestId(), "management must own correlation IDs") + assert.Equal(t, callerReq.GetUpstreamUrl(), wireReq.GetUpstreamUrl()) + assert.Equal(t, callerReq.GetAuthHeaderName(), wireReq.GetAuthHeaderName()) + assert.Equal(t, callerReq.GetAuthHeaderValue(), wireReq.GetAuthHeaderValue()) + assert.Equal(t, callerReq.GetSkipTlsVerify(), wireReq.GetSkipTlsVerify()) + assert.Equal(t, callerReq.GetOllamaFallback(), wireReq.GetOllamaFallback()) + assert.Equal(t, "caller-controlled", callerReq.GetRequestId(), "caller request must not be mutated") + + want := &proto.ModelDiscoveryResult{ + RequestId: wireReq.GetRequestId(), + Models: []*proto.ModelDiscoveryModel{ + {Id: "llama3.2:latest", Label: "llama3.2:latest"}, + }, + Source: "openai_v1_models", + } + server.completeModelDiscovery(conn, want) + + got := <-done + require.NoError(t, got.err) + assert.Equal(t, want, got.result) +} + +func TestDiscoverModels_IgnoresMismatchedCorrelation(t *testing.T) { + server, controller := newModelDiscoveryTestServer(t) + conn, _ := addModelDiscoveryTestConnection( + t, server, controller, "proxy-a", "session-a", "cluster.example.com", nil, true, + ) + + done := callDiscoverModels(server, context.Background(), "account-a", "cluster.example.com", &proto.ModelDiscoveryRequest{ + UpstreamUrl: "http://ollama.internal:11434", + }) + wireReq := <-conn.modelDiscoveryChan + + server.completeModelDiscovery(conn, &proto.ModelDiscoveryResult{RequestId: "wrong-request"}) + select { + case got := <-done: + t.Fatalf("mismatched request completed discovery: %+v", got) + default: + } + + wrongSession := *conn + wrongSession.sessionID = "session-b" + server.completeModelDiscovery(&wrongSession, &proto.ModelDiscoveryResult{RequestId: wireReq.GetRequestId()}) + select { + case got := <-done: + t.Fatalf("mismatched proxy session completed discovery: %+v", got) + default: + } + + server.completeModelDiscovery(conn, &proto.ModelDiscoveryResult{RequestId: wireReq.GetRequestId()}) + got := <-done + require.NoError(t, got.err) + require.NotNil(t, got.result) +} + +func TestDiscoverModels_ReturnsProxyReportedErrorForManagerContext(t *testing.T) { + server, controller := newModelDiscoveryTestServer(t) + conn, _ := addModelDiscoveryTestConnection( + t, server, controller, "proxy-a", "session-a", "cluster.example.com", nil, true, + ) + + done := callDiscoverModels(server, context.Background(), "account-a", "cluster.example.com", &proto.ModelDiscoveryRequest{ + UpstreamUrl: "http://ollama.internal:11434", + }) + wireReq := <-conn.modelDiscoveryChan + server.completeModelDiscovery(conn, &proto.ModelDiscoveryResult{ + RequestId: wireReq.GetRequestId(), + Error: "upstream returned HTTP 401", + }) + + got := <-done + require.NoError(t, got.err) + require.NotNil(t, got.result) + assert.Equal(t, "upstream returned HTTP 401", got.result.GetError()) + assert.Equal(t, wireReq.GetRequestId(), got.result.GetRequestId()) +} + +func TestDiscoverModels_FiltersCapabilityAndAccountScope(t *testing.T) { + tests := []struct { + name string + connectionAccount *string + requestAccount string + supported bool + }{ + { + name: "capability absent", + requestAccount: "account-a", + supported: false, + }, + { + name: "BYOP account mismatch", + connectionAccount: stringPointer("account-b"), + requestAccount: "account-a", + supported: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server, controller := newModelDiscoveryTestServer(t) + addModelDiscoveryTestConnection( + t, server, controller, "proxy-a", "session-a", "cluster.example.com", + tt.connectionAccount, tt.supported, + ) + + result, err := server.DiscoverModels( + context.Background(), + tt.requestAccount, + "cluster.example.com", + &proto.ModelDiscoveryRequest{UpstreamUrl: "http://ollama.internal:11434"}, + ) + require.Nil(t, result) + requireStatusType(t, err, nbstatus.PreconditionFailed) + }) + } +} + +func TestDiscoverModels_RejectsStaleClusterMembershipAfterReconnect(t *testing.T) { + server, controller := newModelDiscoveryTestServer(t) + conn, _ := addModelDiscoveryTestConnection( + t, server, controller, "proxy-a", "new-session", "new-cluster.example.com", nil, true, + ) + // Simulate the stale membership left behind when this proxy ID previously + // connected to another cluster and that superseded session skipped cleanup. + require.NoError(t, controller.RegisterProxyToCluster( + context.Background(), + "old-cluster.example.com", + "proxy-a", + )) + + result, err := server.DiscoverModels( + context.Background(), + "account-a", + "old-cluster.example.com", + &proto.ModelDiscoveryRequest{UpstreamUrl: "http://ollama.internal:11434"}, + ) + + require.Nil(t, result) + requireStatusType(t, err, nbstatus.PreconditionFailed) + select { + case request := <-conn.modelDiscoveryChan: + t.Fatalf("sent credentials to connection in a different cluster: %+v", request) + default: + } +} + +func TestDiscoverModels_CancelAndDisconnectCleanPendingRequest(t *testing.T) { + t.Run("caller cancellation", func(t *testing.T) { + server, controller := newModelDiscoveryTestServer(t) + conn, _ := addModelDiscoveryTestConnection( + t, server, controller, "proxy-a", "session-a", "cluster.example.com", nil, true, + ) + ctx, cancel := context.WithCancel(context.Background()) + done := callDiscoverModels(server, ctx, "account-a", "cluster.example.com", &proto.ModelDiscoveryRequest{ + UpstreamUrl: "http://ollama.internal:11434", + }) + <-conn.modelDiscoveryChan + cancel() + + got := <-done + require.Nil(t, got.result) + requireStatusType(t, got.err, nbstatus.PreconditionFailed) + assert.Empty(t, server.modelDiscoveryPending) + }) + + t.Run("proxy disconnect", func(t *testing.T) { + server, controller := newModelDiscoveryTestServer(t) + conn, _ := addModelDiscoveryTestConnection( + t, server, controller, "proxy-a", "session-a", "cluster.example.com", nil, true, + ) + done := callDiscoverModels(server, context.Background(), "account-a", "cluster.example.com", &proto.ModelDiscoveryRequest{ + UpstreamUrl: "http://ollama.internal:11434", + }) + <-conn.modelDiscoveryChan + server.failModelDiscoveries(conn, rpproxy.ErrModelDiscoveryUnavailable) + + got := <-done + require.Nil(t, got.result) + requireStatusType(t, got.err, nbstatus.PreconditionFailed) + assert.Empty(t, server.modelDiscoveryPending) + }) +} + +func TestDiscoverModels_UsesNextCapableProxyWhenFirstQueueIsFull(t *testing.T) { + server, controller := newModelDiscoveryTestServer(t) + first, _ := addModelDiscoveryTestConnection( + t, server, controller, "proxy-a", "session-a", "cluster.example.com", nil, true, + ) + second, _ := addModelDiscoveryTestConnection( + t, server, controller, "proxy-b", "session-b", "cluster.example.com", nil, true, + ) + first.modelDiscoveryChan = make(chan *proto.ModelDiscoveryRequest, 1) + first.modelDiscoveryChan <- &proto.ModelDiscoveryRequest{RequestId: "occupied"} + + done := callDiscoverModels(server, context.Background(), "account-a", "cluster.example.com", &proto.ModelDiscoveryRequest{ + UpstreamUrl: "http://ollama.internal:11434", + }) + wireReq := <-second.modelDiscoveryChan + server.completeModelDiscovery(second, &proto.ModelDiscoveryResult{RequestId: wireReq.GetRequestId()}) + + got := <-done + require.NoError(t, got.err) + require.NotNil(t, got.result) +} + +func TestSenderSkipsCanceledQueuedModelDiscovery(t *testing.T) { + server, controller := newModelDiscoveryTestServer(t) + conn, _ := addModelDiscoveryTestConnection( + t, server, controller, "proxy-a", "session-a", "cluster.example.com", nil, true, + ) + stream := conn.syncStream.(*syncRecordingStream) + + ctx, cancel := context.WithCancel(context.Background()) + done := callDiscoverModels(server, ctx, "account-a", "cluster.example.com", &proto.ModelDiscoveryRequest{ + UpstreamUrl: "http://ollama.internal:11434", + }) + require.Eventually(t, func() bool { + return len(conn.modelDiscoveryChan) == 1 + }, time.Second, time.Millisecond) + cancel() + got := <-done + require.Nil(t, got.result) + requireStatusType(t, got.err, nbstatus.PreconditionFailed) + + liveRequest := &proto.ModelDiscoveryRequest{RequestId: "live-request"} + livePending := &pendingModelDiscovery{ + proxyID: conn.proxyID, + sessionID: conn.sessionID, + done: make(chan modelDiscoveryCompletion, 1), + } + server.registerModelDiscovery(liveRequest.GetRequestId(), livePending) + defer server.removeModelDiscovery(liveRequest.GetRequestId(), livePending) + conn.modelDiscoveryChan <- liveRequest + + errCh := make(chan error, 1) + go server.sender(conn, errCh) + require.Eventually(t, func() bool { + stream.mu.Lock() + defer stream.mu.Unlock() + return len(stream.sent) == 1 + }, time.Second, time.Millisecond) + + stream.mu.Lock() + sent := append([]*proto.SyncMappingsResponse(nil), stream.sent...) + stream.mu.Unlock() + require.Len(t, sent, 1) + assert.Equal(t, "live-request", sent[0].GetModelDiscoveryRequest().GetRequestId()) +} + +func TestDrainRecv_DispatchesModelDiscoveryResult(t *testing.T) { + server, _ := newModelDiscoveryTestServer(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + conn := &proxyConnection{ + proxyID: "proxy-a", + sessionID: "session-a", + ctx: ctx, + cancel: cancel, + } + pending := &pendingModelDiscovery{ + proxyID: conn.proxyID, + sessionID: conn.sessionID, + done: make(chan modelDiscoveryCompletion, 1), + } + server.registerModelDiscovery("request-a", pending) + stream := &syncRecordingStream{ + recvMsgs: []*proto.SyncMappingsRequest{ + { + Msg: &proto.SyncMappingsRequest_ModelDiscoveryResult{ + ModelDiscoveryResult: &proto.ModelDiscoveryResult{RequestId: "request-a"}, + }, + }, + }, + } + errCh := make(chan error, 1) + + go server.drainRecv(conn, stream, errCh) + + completion := <-pending.done + require.NoError(t, completion.err) + require.NotNil(t, completion.result) + assert.Equal(t, "request-a", completion.result.GetRequestId()) +} + +func TestSendModelDiscoveryRequest_UsesOutOfBandSyncField(t *testing.T) { + stream := &syncRecordingStream{} + conn := &proxyConnection{syncStream: stream} + req := &proto.ModelDiscoveryRequest{RequestId: "request-a"} + + require.NoError(t, conn.sendModelDiscoveryRequest(req)) + require.Len(t, stream.sent, 1) + assert.Empty(t, stream.sent[0].GetMapping()) + assert.Equal(t, req, stream.sent[0].GetModelDiscoveryRequest()) +} + +func stringPointer(value string) *string { + return &value +} + +func requireStatusType(t *testing.T, err error, want nbstatus.Type) { + t.Helper() + require.Error(t, err) + statusErr, ok := nbstatus.FromError(err) + require.True(t, ok) + assert.Equal(t, want, statusErr.Type()) +} diff --git a/management/internals/shared/grpc/proxy.go b/management/internals/shared/grpc/proxy.go index b289f8c71..a234d136a 100644 --- a/management/internals/shared/grpc/proxy.go +++ b/management/internals/shared/grpc/proxy.go @@ -15,6 +15,7 @@ import ( "net/http" "net/url" "os" + "sort" "strconv" "strings" "sync" @@ -29,6 +30,7 @@ import ( "github.com/netbirdio/netbird/shared/management/domain" + "github.com/netbirdio/netbird/management/internals/modules/agentnetwork" "github.com/netbirdio/netbird/management/internals/modules/peers" "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs" "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy" @@ -36,7 +38,6 @@ import ( "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/sessionkey" "github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/management/server/peer" - "github.com/netbirdio/netbird/management/internals/modules/agentnetwork" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/users" proxyauth "github.com/netbirdio/netbird/proxy/auth" @@ -84,6 +85,10 @@ type ProxyServiceServer struct { // Map of connected proxies: proxy_id -> proxy connection connectedProxies sync.Map + // modelDiscoveryPending correlates out-of-band model-discovery results + // received on SyncMappings with the management request waiting for them. + modelDiscoveryMu sync.Mutex + modelDiscoveryPending map[string]*pendingModelDiscovery // Manager for access logs accessLogManager accesslogs.Manager @@ -143,6 +148,8 @@ const defaultProxyTokenTTL = 5 * time.Minute const defaultSnapshotBatchSize = 500 +const modelDiscoveryQueueSize = 16 + func snapshotBatchSizeFromEnv() int { if v := os.Getenv("NB_PROXY_SNAPSHOT_BATCH_SIZE"); v != "" { if n, err := strconv.Atoi(v); err == nil && n > 0 { @@ -171,10 +178,26 @@ type proxyConnection struct { stream proto.ProxyService_GetMappingUpdateServer // syncStream is set when the proxy connected via SyncMappings. // When non-nil, the sender goroutine uses this instead of stream. - syncStream proto.ProxyService_SyncMappingsServer - sendChan chan *proto.GetMappingUpdateResponse - ctx context.Context - cancel context.CancelFunc + syncStream proto.ProxyService_SyncMappingsServer + sendChan chan *proto.GetMappingUpdateResponse + modelDiscoveryChan chan *proto.ModelDiscoveryRequest + // ready closes after the initial snapshot completes and the sender + // goroutine is about to start. Discovery never competes with snapshot + // delivery on the stream. + ready chan struct{} + ctx context.Context + cancel context.CancelFunc +} + +type modelDiscoveryCompletion struct { + result *proto.ModelDiscoveryResult + err error +} + +type pendingModelDiscovery struct { + proxyID string + sessionID string + done chan modelDiscoveryCompletion } func enforceAccountScope(ctx context.Context, requestAccountID string) error { @@ -192,17 +215,18 @@ func enforceAccountScope(ctx context.Context, requestAccountID string) error { func NewProxyServiceServer(accessLogMgr accesslogs.Manager, tokenStore *OneTimeTokenStore, pkceStore *PKCEVerifierStore, oidcConfig ProxyOIDCConfig, peersManager peers.Manager, usersManager users.Manager, idpManager idp.Manager, proxyMgr proxy.Manager, tokenChecker ProxyTokenChecker) *ProxyServiceServer { ctx, cancel := context.WithCancel(context.Background()) s := &ProxyServiceServer{ - accessLogManager: accessLogMgr, - oidcConfig: oidcConfig, - tokenStore: tokenStore, - pkceVerifierStore: pkceStore, - peersManager: peersManager, - usersManager: usersManager, - idpManager: idpManager, - proxyManager: proxyMgr, - tokenChecker: tokenChecker, - snapshotBatchSize: snapshotBatchSizeFromEnv(), - cancel: cancel, + accessLogManager: accessLogMgr, + oidcConfig: oidcConfig, + tokenStore: tokenStore, + pkceVerifierStore: pkceStore, + peersManager: peersManager, + usersManager: usersManager, + idpManager: idpManager, + proxyManager: proxyMgr, + tokenChecker: tokenChecker, + snapshotBatchSize: snapshotBatchSizeFromEnv(), + modelDiscoveryPending: make(map[string]*pendingModelDiscovery), + cancel: cancel, } go s.cleanupStaleProxies(ctx) return s @@ -392,6 +416,7 @@ func (s *ProxyServiceServer) GetMappingUpdate(req *proto.GetMappingUpdateRequest return fmt.Errorf("send snapshot to proxy %s: %w", params.proxyID, err) } + close(conn.ready) errChan := make(chan error, 2) go s.sender(conn, errChan) @@ -425,9 +450,10 @@ func (s *ProxyServiceServer) SyncMappings(stream proto.ProxyService_SyncMappings return fmt.Errorf("send snapshot to proxy %s: %w", params.proxyID, err) } + close(conn.ready) errChan := make(chan error, 2) go s.sender(conn, errChan) - go s.drainRecv(stream, errChan) + go s.drainRecv(conn, stream, errChan) return s.serveProxyConnection(conn, proxyRecord, errChan, true) } @@ -496,6 +522,8 @@ func (s *ProxyServiceServer) registerProxyConnection(ctx context.Context, params connSeed.tokenID = tokenID connSeed.capabilities = params.capabilities connSeed.sendChan = make(chan *proto.GetMappingUpdateResponse, 100) + connSeed.modelDiscoveryChan = make(chan *proto.ModelDiscoveryRequest, modelDiscoveryQueueSize) + connSeed.ready = make(chan struct{}) connSeed.ctx = connCtx connSeed.cancel = cancel @@ -543,10 +571,13 @@ func (s *ProxyServiceServer) supersedePriorConnection(proxyID, newSessionID stri // cleanupFailedSnapshot removes the connection from the cluster and store // after a snapshot send failure. func (s *ProxyServiceServer) cleanupFailedSnapshot(ctx context.Context, conn *proxyConnection) { + s.failModelDiscoveries(conn, proxy.ErrModelDiscoveryUnavailable) if s.connectedProxies.CompareAndDelete(conn.proxyID, conn) { if err := s.proxyController.UnregisterProxyFromCluster(context.Background(), conn.address, conn.proxyID); err != nil { log.WithContext(ctx).Debugf("cleanup after snapshot failure for proxy %s: %v", conn.proxyID, err) } + } else { + s.unregisterSupersededProxyCluster(ctx, conn) } conn.cancel() if err := s.proxyManager.Disconnect(context.Background(), conn.proxyID, conn.sessionID); err != nil { @@ -554,15 +585,19 @@ func (s *ProxyServiceServer) cleanupFailedSnapshot(ctx context.Context, conn *pr } } -// drainRecv consumes and discards messages from a bidirectional stream. -// The proxy sends an ack for every incremental update; we don't need them -// after the snapshot phase. Recv errors are forwarded to errChan. -func (s *ProxyServiceServer) drainRecv(stream proto.ProxyService_SyncMappingsServer, errChan chan<- error) { +// drainRecv consumes post-snapshot messages from a bidirectional stream. +// Incremental mapping acks need no action; model-discovery results are +// dispatched to the correlated management caller. +func (s *ProxyServiceServer) drainRecv(conn *proxyConnection, stream proto.ProxyService_SyncMappingsServer, errChan chan<- error) { for { - if _, err := stream.Recv(); err != nil { + msg, err := stream.Recv() + if err != nil { errChan <- err return } + if result := msg.GetModelDiscoveryResult(); result != nil { + s.completeModelDiscovery(conn, result) + } } } @@ -599,7 +634,9 @@ func (s *ProxyServiceServer) serveProxyConnection(conn *proxyConnection, proxyRe // disconnectProxy removes the connection from cluster and store, unless it // has already been superseded by a newer connection. func (s *ProxyServiceServer) disconnectProxy(conn *proxyConnection) { + s.failModelDiscoveries(conn, proxy.ErrModelDiscoveryUnavailable) if !s.connectedProxies.CompareAndDelete(conn.proxyID, conn) { + s.unregisterSupersededProxyCluster(context.Background(), conn) log.Infof("Proxy %s session %s: skipping cleanup, superseded by new connection", conn.proxyID, conn.sessionID) conn.cancel() return @@ -616,6 +653,26 @@ func (s *ProxyServiceServer) disconnectProxy(conn *proxyConnection) { log.Infof("Proxy %s session %s disconnected", conn.proxyID, conn.sessionID) } +// unregisterSupersededProxyCluster removes membership owned only by an old +// session after the same proxy ID reconnects at a different address. When the +// address is unchanged, the membership is shared with the replacement and +// must remain registered. +func (s *ProxyServiceServer) unregisterSupersededProxyCluster(ctx context.Context, conn *proxyConnection) { + if conn == nil || s.proxyController == nil { + return + } + currentValue, ok := s.connectedProxies.Load(conn.proxyID) + if ok { + current := currentValue.(*proxyConnection) + if current == conn || current.address == conn.address { + return + } + } + if err := s.proxyController.UnregisterProxyFromCluster(ctx, conn.address, conn.proxyID); err != nil { + log.WithContext(ctx).Debugf("cleanup superseded cluster membership for proxy %s: %v", conn.proxyID, err) + } +} + // sendSnapshotSync sends the initial snapshot with back-pressure: it sends // one batch, then waits for the proxy to ack before sending the next. func (s *ProxyServiceServer) sendSnapshotSync(ctx context.Context, conn *proxyConnection, stream proto.ProxyService_SyncMappingsServer) error { @@ -852,6 +909,20 @@ func (s *ProxyServiceServer) sender(conn *proxyConnection, errChan chan<- error) return } log.WithContext(conn.ctx).Tracef("Send response to proxy %s", conn.proxyID) + case req := <-conn.modelDiscoveryChan: + // The API caller may have timed out while this request waited + // behind other stream work. Pending correlation is removed on + // cancellation, so skip stale probes before they reach the + // proxy and expose a stored credential unnecessarily. + if !s.isModelDiscoveryPendingFor(conn, req.GetRequestId()) { + continue + } + if err := conn.sendModelDiscoveryRequest(req); err != nil { + errChan <- err + log.WithContext(conn.ctx).Tracef("Failed to send model discovery request to proxy %s: %v", conn.proxyID, err) + return + } + log.WithContext(conn.ctx).Tracef("Sent model discovery request to proxy %s", conn.proxyID) case <-conn.ctx.Done(): return } @@ -869,6 +940,15 @@ func (conn *proxyConnection) sendResponse(resp *proto.GetMappingUpdateResponse) return conn.stream.Send(resp) } +func (conn *proxyConnection) sendModelDiscoveryRequest(req *proto.ModelDiscoveryRequest) error { + if conn.syncStream == nil { + return proxy.ErrModelDiscoveryUnavailable + } + return conn.syncStream.Send(&proto.SyncMappingsResponse{ + ModelDiscoveryRequest: req, + }) +} + // SendAccessLog processes access log from proxy func (s *ProxyServiceServer) SendAccessLog(ctx context.Context, req *proto.SendAccessLogRequest) (*proto.SendAccessLogResponse, error) { accessLog := req.GetLog() @@ -1017,6 +1097,181 @@ func (s *ProxyServiceServer) GetConnectedProxyURLs() []string { return urls } +// DiscoverModels sends one correlated model-discovery request to a capable +// proxy in clusterAddr and waits for its result. Only the stored connection's +// cluster/account scope is used for routing; no routing identity is carried in +// the request delivered to the proxy. +func (s *ProxyServiceServer) DiscoverModels(ctx context.Context, accountID, clusterAddr string, req *proto.ModelDiscoveryRequest) (*proto.ModelDiscoveryResult, error) { + if req == nil { + return nil, nbstatus.Errorf(nbstatus.InvalidArgument, "model discovery request is required") + } + if strings.TrimSpace(accountID) == "" { + return nil, nbstatus.Errorf(nbstatus.InvalidArgument, "account ID is required for model discovery") + } + if strings.TrimSpace(clusterAddr) == "" { + return nil, nbstatus.Errorf(nbstatus.PreconditionFailed, "agent network proxy cluster is not configured") + } + if s.proxyController == nil { + return nil, nbstatus.Errorf(nbstatus.PreconditionFailed, "model discovery is unavailable for the configured proxy cluster") + } + + proxyIDs := s.proxyController.GetProxiesForCluster(clusterAddr) + sort.Strings(proxyIDs) + + request := &proto.ModelDiscoveryRequest{ + RequestId: uuid.NewString(), + UpstreamUrl: req.GetUpstreamUrl(), + AuthHeaderName: req.GetAuthHeaderName(), + AuthHeaderValue: req.GetAuthHeaderValue(), + SkipTlsVerify: req.GetSkipTlsVerify(), + OllamaFallback: req.GetOllamaFallback(), + } + + for _, proxyID := range proxyIDs { + connVal, ok := s.connectedProxies.Load(proxyID) + if !ok { + continue + } + conn := connVal.(*proxyConnection) + // Cluster membership can briefly retain a proxy ID from a superseded + // session. The live connection is authoritative: never send provider + // credentials unless it is connected to the exact requested cluster. + if conn.address != clusterAddr { + continue + } + if !modelDiscoveryCapable(conn, accountID) { + continue + } + + pending := &pendingModelDiscovery{ + proxyID: conn.proxyID, + sessionID: conn.sessionID, + done: make(chan modelDiscoveryCompletion, 1), + } + s.registerModelDiscovery(request.GetRequestId(), pending) + + queued := false + select { + case conn.modelDiscoveryChan <- request: + queued = true + case <-ctx.Done(): + s.removeModelDiscovery(request.GetRequestId(), pending) + return nil, modelDiscoveryContextError(ctx.Err()) + default: + s.removeModelDiscovery(request.GetRequestId(), pending) + } + if !queued { + continue + } + + defer s.removeModelDiscovery(request.GetRequestId(), pending) + select { + case completion := <-pending.done: + if completion.err != nil { + return nil, nbstatus.Errorf(nbstatus.PreconditionFailed, "model discovery proxy disconnected") + } + return completion.result, nil + case <-ctx.Done(): + return nil, modelDiscoveryContextError(ctx.Err()) + case <-conn.ctx.Done(): + return nil, nbstatus.Errorf(nbstatus.PreconditionFailed, "model discovery proxy disconnected") + } + } + + return nil, nbstatus.Errorf(nbstatus.PreconditionFailed, "no connected proxy in the configured cluster supports model discovery") +} + +func modelDiscoveryCapable(conn *proxyConnection, accountID string) bool { + if conn == nil || conn.syncStream == nil || conn.modelDiscoveryChan == nil || conn.ready == nil { + return false + } + if conn.ctx == nil || conn.ctx.Err() != nil { + return false + } + if conn.accountID != nil && *conn.accountID != accountID { + return false + } + if conn.capabilities == nil || !conn.capabilities.GetSupportsModelDiscovery() { + return false + } + select { + case <-conn.ready: + return true + default: + return false + } +} + +func modelDiscoveryContextError(err error) error { + if errors.Is(err, context.DeadlineExceeded) { + return nbstatus.Errorf(nbstatus.PreconditionFailed, "model discovery timed out") + } + return nbstatus.Errorf(nbstatus.PreconditionFailed, "model discovery was canceled") +} + +func (s *ProxyServiceServer) registerModelDiscovery(requestID string, pending *pendingModelDiscovery) { + s.modelDiscoveryMu.Lock() + defer s.modelDiscoveryMu.Unlock() + if s.modelDiscoveryPending == nil { + s.modelDiscoveryPending = make(map[string]*pendingModelDiscovery) + } + s.modelDiscoveryPending[requestID] = pending +} + +func (s *ProxyServiceServer) removeModelDiscovery(requestID string, pending *pendingModelDiscovery) { + s.modelDiscoveryMu.Lock() + defer s.modelDiscoveryMu.Unlock() + if s.modelDiscoveryPending[requestID] == pending { + delete(s.modelDiscoveryPending, requestID) + } +} + +func (s *ProxyServiceServer) isModelDiscoveryPendingFor(conn *proxyConnection, requestID string) bool { + if conn == nil || requestID == "" { + return false + } + s.modelDiscoveryMu.Lock() + defer s.modelDiscoveryMu.Unlock() + pending := s.modelDiscoveryPending[requestID] + return pending != nil && pending.proxyID == conn.proxyID && pending.sessionID == conn.sessionID +} + +func (s *ProxyServiceServer) completeModelDiscovery(conn *proxyConnection, result *proto.ModelDiscoveryResult) { + if conn == nil || result == nil || result.GetRequestId() == "" { + return + } + s.modelDiscoveryMu.Lock() + pending := s.modelDiscoveryPending[result.GetRequestId()] + s.modelDiscoveryMu.Unlock() + if pending == nil || pending.proxyID != conn.proxyID || pending.sessionID != conn.sessionID { + return + } + select { + case pending.done <- modelDiscoveryCompletion{result: result}: + default: + } +} + +func (s *ProxyServiceServer) failModelDiscoveries(conn *proxyConnection, err error) { + if conn == nil { + return + } + s.modelDiscoveryMu.Lock() + pending := make([]*pendingModelDiscovery, 0) + for _, item := range s.modelDiscoveryPending { + if item.proxyID == conn.proxyID && item.sessionID == conn.sessionID { + pending = append(pending, item) + } + } + s.modelDiscoveryMu.Unlock() + for _, item := range pending { + select { + case item.done <- modelDiscoveryCompletion{err: err}: + default: + } + } +} + // SendServiceUpdateToCluster sends a service update to all proxy servers in a specific cluster. // If clusterAddr is empty, broadcasts to all connected proxy servers (backward compatibility). // For create/update operations a unique one-time auth token is generated per @@ -1049,6 +1304,12 @@ func (s *ProxyServiceServer) SendServiceUpdateToCluster(ctx context.Context, upd continue } conn := connVal.(*proxyConnection) + // Membership can retain this proxy ID from a superseded session in a + // different cluster. The live connection address is authoritative, + // especially because Agent Network mappings can contain credentials. + if conn.address != clusterAddr { + continue + } if conn.accountID != nil && update.AccountId != "" && *conn.accountID != update.AccountId { continue } diff --git a/management/internals/shared/grpc/proxy_test.go b/management/internals/shared/grpc/proxy_test.go index 0379edc6d..b92c37549 100644 --- a/management/internals/shared/grpc/proxy_test.go +++ b/management/internals/shared/grpc/proxy_test.go @@ -42,6 +42,10 @@ func newTestProxyController() *testProxyController { func (c *testProxyController) SendServiceUpdateToCluster(_ context.Context, _ string, _ *proto.ProxyMapping, _ string) { } +func (c *testProxyController) DiscoverModels(_ context.Context, _, _ string, _ *proto.ModelDiscoveryRequest) (*proto.ModelDiscoveryResult, error) { + return nil, proxy.ErrModelDiscoveryUnavailable +} + func (c *testProxyController) GetOIDCValidationConfig() proxy.OIDCValidationConfig { return proxy.OIDCValidationConfig{} } @@ -218,6 +222,65 @@ func TestSendServiceUpdateToCluster_DeleteNoToken(t *testing.T) { assert.Empty(t, msg2.AuthToken) } +func TestSendServiceUpdateToCluster_RejectsStaleClusterMembership(t *testing.T) { + ctx := context.Background() + s := &ProxyServiceServer{ + tokenStore: NewOneTimeTokenStore(ctx, testCacheStore(t)), + } + controller := newTestProxyController() + s.SetProxyController(controller) + + ch := registerFakeProxy(s, "proxy-a", "new-cluster.example.com") + require.NoError(t, controller.RegisterProxyToCluster(ctx, "old-cluster.example.com", "proxy-a")) + + s.SendServiceUpdateToCluster(ctx, &proto.ProxyMapping{ + Type: proto.ProxyMappingUpdateType_UPDATE_TYPE_CREATED, + Id: "agent-network-provider-1", + AccountId: "account-1", + Domain: "agent.example.com", + Path: []*proto.PathMapping{ + {Path: "/", Target: "http://ollama.internal:11434/"}, + }, + }, "old-cluster.example.com") + + assert.True(t, drainEmpty(ch), "stale membership must not route a mapping to a different live cluster") +} + +func TestDisconnectProxy_RemovesSupersededMembershipFromOldCluster(t *testing.T) { + controller := newTestProxyController() + server := &ProxyServiceServer{proxyController: controller} + oldCtx, cancelOld := context.WithCancel(context.Background()) + newCtx, cancelNew := context.WithCancel(context.Background()) + t.Cleanup(cancelOld) + t.Cleanup(cancelNew) + + oldConnection := &proxyConnection{ + proxyID: "proxy-a", + sessionID: "old-session", + address: "old-cluster.example.com", + ctx: oldCtx, + cancel: cancelOld, + } + newConnection := &proxyConnection{ + proxyID: "proxy-a", + sessionID: "new-session", + address: "new-cluster.example.com", + ctx: newCtx, + cancel: cancelNew, + } + require.NoError(t, controller.RegisterProxyToCluster(context.Background(), oldConnection.address, oldConnection.proxyID)) + require.NoError(t, controller.RegisterProxyToCluster(context.Background(), newConnection.address, newConnection.proxyID)) + server.connectedProxies.Store(newConnection.proxyID, newConnection) + + server.disconnectProxy(oldConnection) + + assert.Empty(t, controller.GetProxiesForCluster(oldConnection.address)) + assert.Equal(t, []string{"proxy-a"}, controller.GetProxiesForCluster(newConnection.address)) + current, ok := server.connectedProxies.Load(newConnection.proxyID) + require.True(t, ok) + assert.Same(t, newConnection, current) +} + func TestSendServiceUpdate_UniqueTokensPerProxy(t *testing.T) { ctx := context.Background() tokenStore := NewOneTimeTokenStore(ctx, testCacheStore(t)) diff --git a/proxy/internal/modeldiscovery/discovery.go b/proxy/internal/modeldiscovery/discovery.go new file mode 100644 index 000000000..184a563b7 --- /dev/null +++ b/proxy/internal/modeldiscovery/discovery.go @@ -0,0 +1,337 @@ +// Package modeldiscovery fetches and normalizes model catalogs from +// Agent Network provider endpoints. Discovery always runs on the proxy so +// it observes the same network path as inference traffic. +package modeldiscovery + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "path" + "sort" + "strings" + "time" + "unicode" + "unicode/utf8" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/proxy/internal/roundtrip" +) + +const ( + SourceOpenAIV1Models = "openai_v1_models" + SourceOllamaAPITags = "ollama_api_tags" + + defaultTimeout = 5 * time.Second + maxResponseBytes = 1 << 20 // 1 MiB, after HTTP decompression. + maxUpstreamURLBytes = 4096 + maxHeaderNameBytes = 256 + maxHeaderValueBytes = 64 << 10 + maxModels = 500 + maxModelIDBytes = 512 +) + +// Request contains the provider-owned values management resolved from the +// persisted provider record. Callers must not populate these fields from a +// dashboard-supplied URL or credential. +type Request struct { + UpstreamURL string + AuthHeaderName string + AuthHeaderValue string + SkipTLSVerify bool + AllowOllamaFallback bool +} + +// Model is the deliberately small response surface returned to management. +// Arbitrary fields supplied by an upstream never cross the control channel. +type Model struct { + ID string + Label string +} + +// Result is a normalized model catalog and the endpoint shape that supplied +// it. +type Result struct { + Models []Model + Source string +} + +// Discoverer owns the HTTP client used for provider probes. +type Discoverer struct { + client *http.Client + timeout time.Duration + bodyLimit int64 +} + +// New constructs a direct-upstream discoverer. The transport is the same +// host-network transport family used by Agent Network inference routes. +func New(logger *log.Logger) *Discoverer { + return newWithTransport(roundtrip.NewDirectOnly(logger)) +} + +func newWithTransport(transport http.RoundTripper) *Discoverer { + return &Discoverer{ + client: &http.Client{ + Transport: transport, + // Redirects could move a credentialed request away from the + // persisted provider origin. Discovery never follows them. + CheckRedirect: func(_ *http.Request, _ []*http.Request) error { + return http.ErrUseLastResponse + }, + }, + timeout: defaultTimeout, + bodyLimit: maxResponseBytes, + } +} + +// Discover queries the OpenAI-compatible model-list endpoint. Ollama's native +// tags endpoint is attempted only when explicitly enabled and the primary +// endpoint reports that the route does not exist. +func (d *Discoverer) Discover(ctx context.Context, in Request) (Result, error) { + if d == nil || d.client == nil { + return Result{}, errors.New("model discovery client is unavailable") + } + if err := validateRequest(in); err != nil { + return Result{}, err + } + + timeout := d.timeout + if timeout <= 0 || timeout > defaultTimeout { + timeout = defaultTimeout + } + probeCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + models, err := d.fetchOpenAIModels(probeCtx, in) + if err == nil { + return Result{Models: models, Source: SourceOpenAIV1Models}, nil + } + if !in.AllowOllamaFallback || !isMissingEndpoint(err) { + return Result{}, err + } + + models, err = d.fetchOllamaTags(probeCtx, in) + if err != nil { + return Result{}, err + } + return Result{Models: models, Source: SourceOllamaAPITags}, nil +} + +func validateRequest(in Request) error { + rawURL := strings.TrimSpace(in.UpstreamURL) + if rawURL == "" { + return errors.New("model discovery upstream URL is required") + } + if len(rawURL) > maxUpstreamURLBytes { + return errors.New("model discovery upstream URL is too long") + } + if len(in.AuthHeaderName) > maxHeaderNameBytes || len(in.AuthHeaderValue) > maxHeaderValueBytes { + return errors.New("model discovery authentication header is too large") + } + if (in.AuthHeaderName == "") != (in.AuthHeaderValue == "") { + return errors.New("model discovery authentication header is incomplete") + } + if strings.ContainsAny(in.AuthHeaderName, "\r\n") || strings.ContainsAny(in.AuthHeaderValue, "\r\n") { + return errors.New("model discovery authentication header is invalid") + } + if in.AuthHeaderName != "" && !strings.EqualFold(in.AuthHeaderName, "Authorization") { + return errors.New("model discovery authentication header is unsupported") + } + return nil +} + +func (d *Discoverer) fetchOpenAIModels(ctx context.Context, in Request) ([]Model, error) { + body, err := d.fetch(ctx, in, "v1/models") + if err != nil { + return nil, err + } + + var payload struct { + Data *[]struct { + ID string `json:"id"` + } `json:"data"` + } + if err := json.Unmarshal(body, &payload); err != nil || payload.Data == nil { + return nil, errors.New("upstream returned invalid OpenAI model-list JSON") + } + + ids := make([]string, 0, len(*payload.Data)) + for _, model := range *payload.Data { + ids = append(ids, model.ID) + } + return normalize(ids) +} + +func (d *Discoverer) fetchOllamaTags(ctx context.Context, in Request) ([]Model, error) { + body, err := d.fetch(ctx, in, "api/tags") + if err != nil { + return nil, err + } + + var payload struct { + Models *[]struct { + Name string `json:"name"` + Model string `json:"model"` + } `json:"models"` + } + if err := json.Unmarshal(body, &payload); err != nil || payload.Models == nil { + return nil, errors.New("upstream returned invalid Ollama tags JSON") + } + + ids := make([]string, 0, len(*payload.Models)) + for _, model := range *payload.Models { + id := model.Model + if strings.TrimSpace(id) == "" { + id = model.Name + } + ids = append(ids, id) + } + return normalize(ids) +} + +func (d *Discoverer) fetch(ctx context.Context, in Request, endpointPath string) ([]byte, error) { + endpoint, err := buildEndpointURL(in.UpstreamURL, endpointPath) + if err != nil { + return nil, err + } + + reqCtx := roundtrip.WithDirectUpstream(ctx) + if in.SkipTLSVerify { + reqCtx = roundtrip.WithSkipTLSVerify(reqCtx) + } + req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, endpoint.String(), nil) + if err != nil { + return nil, errors.New("create model discovery request") + } + req.Header.Set("Accept", "application/json") + if in.AuthHeaderName != "" { + // Phase 3 is Ollama-only. Canonicalizing the sole catalog-owned + // credential header keeps the control message from becoming a generic + // arbitrary-header primitive. + req.Header.Set("Authorization", in.AuthHeaderValue) + } + + resp, err := d.client.Do(req) + if err != nil { + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return nil, errors.New("model discovery timed out") + } + if errors.Is(ctx.Err(), context.Canceled) { + return nil, context.Canceled + } + // Deliberately omit the underlying error: net/http errors include the + // internal URL, which should not be reflected through the public API. + return nil, errors.New("model discovery request failed") + } + defer func() { + _ = resp.Body.Close() + }() + + if resp.StatusCode != http.StatusOK { + return nil, &upstreamStatusError{statusCode: resp.StatusCode} + } + + limit := d.bodyLimit + if limit <= 0 || limit > maxResponseBytes { + limit = maxResponseBytes + } + if resp.ContentLength > limit { + return nil, errors.New("model discovery response is too large") + } + body, err := io.ReadAll(io.LimitReader(resp.Body, limit+1)) + if err != nil { + return nil, errors.New("read model discovery response") + } + if int64(len(body)) > limit { + return nil, errors.New("model discovery response is too large") + } + return body, nil +} + +func buildEndpointURL(rawURL, endpointPath string) (*url.URL, error) { + parsed, err := url.Parse(strings.TrimSpace(rawURL)) + if err != nil || parsed.Host == "" || parsed.Hostname() == "" || parsed.Opaque != "" { + return nil, errors.New("model discovery upstream URL is invalid") + } + switch strings.ToLower(parsed.Scheme) { + case "http": + parsed.Scheme = "http" + case "https": + parsed.Scheme = "https" + default: + return nil, errors.New("model discovery upstream URL must use http or https") + } + if parsed.User != nil { + return nil, errors.New("model discovery upstream URL must not contain credentials") + } + + // Match Agent Network routing semantics: the static discovery path is + // appended to any persisted base path. Queries and fragments on a provider + // URL are not forwarded to inference and are likewise excluded here. + parsed.Path = "/" + strings.TrimPrefix(path.Join(parsed.Path, endpointPath), "/") + parsed.RawPath = "" + parsed.RawQuery = "" + parsed.ForceQuery = false + parsed.Fragment = "" + return parsed, nil +} + +func normalize(ids []string) ([]Model, error) { + if len(ids) > maxModels { + return nil, errors.New("upstream returned too many models") + } + + seen := make(map[string]struct{}, len(ids)) + normalized := make([]string, 0, len(ids)) + for _, raw := range ids { + id := strings.TrimSpace(raw) + if !validModelID(id) { + continue + } + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + normalized = append(normalized, id) + } + sort.Strings(normalized) + + models := make([]Model, 0, len(normalized)) + for _, id := range normalized { + models = append(models, Model{ID: id, Label: id}) + } + return models, nil +} + +func validModelID(id string) bool { + if id == "" || len(id) > maxModelIDBytes || !utf8.ValidString(id) { + return false + } + for _, r := range id { + if unicode.IsControl(r) { + return false + } + } + return true +} + +type upstreamStatusError struct { + statusCode int +} + +func (e *upstreamStatusError) Error() string { + return fmt.Sprintf("upstream returned HTTP %d", e.statusCode) +} + +func isMissingEndpoint(err error) bool { + var statusErr *upstreamStatusError + if !errors.As(err, &statusErr) { + return false + } + return statusErr.statusCode == http.StatusNotFound || statusErr.statusCode == http.StatusMethodNotAllowed +} diff --git a/proxy/internal/modeldiscovery/discovery_test.go b/proxy/internal/modeldiscovery/discovery_test.go new file mode 100644 index 000000000..52eb71caa --- /dev/null +++ b/proxy/internal/modeldiscovery/discovery_test.go @@ -0,0 +1,315 @@ +package modeldiscovery + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "strings" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDiscoverOpenAIModels(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + assert.Equal(t, "/gateway/v1/models", r.URL.Path) + assert.Empty(t, r.URL.RawQuery) + assert.Equal(t, "application/json", r.Header.Get("Accept")) + assert.Equal(t, "Bearer secret", r.Header.Get("Authorization")) + _, _ = io.WriteString(w, `{ + "object": "list", + "data": [ + {"id": " qwen2.5:latest ", "owned_by": "ignored"}, + {"id": "llama3.2:latest"}, + {"id": "llama3.2:latest"}, + {"id": ""}, + {"id": "bad\u0000id"} + ] + }`) + })) + defer server.Close() + + discoverer := newWithTransport(http.DefaultTransport) + result, err := discoverer.Discover(context.Background(), Request{ + UpstreamURL: server.URL + "/gateway/?ignored=true#fragment", + AuthHeaderName: "authorization", + AuthHeaderValue: "Bearer secret", + }) + require.NoError(t, err) + assert.Equal(t, SourceOpenAIV1Models, result.Source) + assert.Equal(t, []Model{ + {ID: "llama3.2:latest", Label: "llama3.2:latest"}, + {ID: "qwen2.5:latest", Label: "qwen2.5:latest"}, + }, result.Models) +} + +func TestDiscoverOllamaFallback(t *testing.T) { + t.Parallel() + + var primaryCalls atomic.Int32 + var fallbackCalls atomic.Int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v1/models": + primaryCalls.Add(1) + http.NotFound(w, r) + case "/api/tags": + fallbackCalls.Add(1) + _, _ = io.WriteString(w, `{ + "models": [ + {"name": "ignored-name", "model": "gemma3:latest"}, + {"name": "llama3.2:latest", "model": ""} + ] + }`) + default: + http.Error(w, "unexpected path", http.StatusInternalServerError) + } + })) + defer server.Close() + + result, err := newWithTransport(http.DefaultTransport).Discover(context.Background(), Request{ + UpstreamURL: server.URL, + AllowOllamaFallback: true, + }) + require.NoError(t, err) + assert.Equal(t, int32(1), primaryCalls.Load()) + assert.Equal(t, int32(1), fallbackCalls.Load()) + assert.Equal(t, SourceOllamaAPITags, result.Source) + assert.Equal(t, []Model{ + {ID: "gemma3:latest", Label: "gemma3:latest"}, + {ID: "llama3.2:latest", Label: "llama3.2:latest"}, + }, result.Models) +} + +func TestDiscoverDoesNotFallbackOnAuthenticationFailure(t *testing.T) { + t.Parallel() + + var fallbackCalls atomic.Int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/api/tags" { + fallbackCalls.Add(1) + } + http.Error(w, "secret upstream body", http.StatusUnauthorized) + })) + defer server.Close() + + _, err := newWithTransport(http.DefaultTransport).Discover(context.Background(), Request{ + UpstreamURL: server.URL, + AllowOllamaFallback: true, + }) + require.EqualError(t, err, "upstream returned HTTP 401") + assert.Zero(t, fallbackCalls.Load()) + assert.NotContains(t, err.Error(), "secret upstream body") + assert.NotContains(t, err.Error(), server.URL) +} + +func TestDiscoverDoesNotFollowRedirectsOrLeakAuth(t *testing.T) { + t.Parallel() + + var redirectedCalls atomic.Int32 + redirected := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + redirectedCalls.Add(1) + assert.Empty(t, r.Header.Get("Authorization")) + _, _ = io.WriteString(w, `{"data":[]}`) + })) + defer redirected.Close() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, redirected.URL+"/captured", http.StatusFound) + })) + defer server.Close() + + _, err := newWithTransport(http.DefaultTransport).Discover(context.Background(), Request{ + UpstreamURL: server.URL, + AuthHeaderName: "Authorization", + AuthHeaderValue: "Bearer must-not-leak", + }) + require.EqualError(t, err, "upstream returned HTTP 302") + assert.Zero(t, redirectedCalls.Load()) +} + +func TestDiscoverEnforcesResponseLimit(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = io.WriteString(w, `{"data":[{"id":"llama3.2:latest"}]}`) + })) + defer server.Close() + + discoverer := newWithTransport(http.DefaultTransport) + discoverer.bodyLimit = 16 + _, err := discoverer.Discover(context.Background(), Request{UpstreamURL: server.URL}) + require.EqualError(t, err, "model discovery response is too large") +} + +func TestDiscoverEnforcesTimeout(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + select { + case <-r.Context().Done(): + case <-time.After(time.Second): + _, _ = io.WriteString(w, `{"data":[]}`) + } + })) + defer server.Close() + + discoverer := newWithTransport(http.DefaultTransport) + discoverer.timeout = 30 * time.Millisecond + _, err := discoverer.Discover(context.Background(), Request{UpstreamURL: server.URL}) + require.EqualError(t, err, "model discovery timed out") +} + +func TestDiscoverHonorsSkipTLSVerification(t *testing.T) { + t.Parallel() + + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = io.WriteString(w, `{"data":[]}`) + })) + defer server.Close() + + discoverer := New(nil) + _, err := discoverer.Discover(context.Background(), Request{UpstreamURL: server.URL}) + require.EqualError(t, err, "model discovery request failed") + + result, err := discoverer.Discover(context.Background(), Request{ + UpstreamURL: server.URL, + SkipTLSVerify: true, + }) + require.NoError(t, err) + assert.Empty(t, result.Models) +} + +func TestDiscoverRejectsUnsafeRequestValues(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + request Request + wantErr string + }{ + { + name: "unsupported scheme", + request: Request{UpstreamURL: "file:///etc/passwd"}, + wantErr: "model discovery upstream URL is invalid", + }, + { + name: "URL credentials", + request: Request{UpstreamURL: "http://user:pass@example.com"}, + wantErr: "model discovery upstream URL must not contain credentials", + }, + { + name: "missing hostname", + request: Request{UpstreamURL: "http://:11434"}, + wantErr: "model discovery upstream URL is invalid", + }, + { + name: "incomplete auth header", + request: Request{ + UpstreamURL: "http://example.com", + AuthHeaderName: "Authorization", + }, + wantErr: "model discovery authentication header is incomplete", + }, + { + name: "header injection", + request: Request{ + UpstreamURL: "http://example.com", + AuthHeaderName: "Authorization", + AuthHeaderValue: "Bearer safe\r\nX-Evil: yes", + }, + wantErr: "model discovery authentication header is invalid", + }, + { + name: "unsupported auth header", + request: Request{ + UpstreamURL: "http://example.com", + AuthHeaderName: "X-API-Key", + AuthHeaderValue: "secret", + }, + wantErr: "model discovery authentication header is unsupported", + }, + { + name: "oversized URL", + request: Request{ + UpstreamURL: "http://example.com/" + strings.Repeat("a", maxUpstreamURLBytes), + }, + wantErr: "model discovery upstream URL is too long", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + _, err := newWithTransport(http.DefaultTransport).Discover(context.Background(), test.request) + require.EqualError(t, err, test.wantErr) + }) + } +} + +func TestDiscoverRejectsInvalidPayloads(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + status int + body string + wantErr string + }{ + { + name: "malformed JSON", + status: http.StatusOK, + body: `{"data":`, + wantErr: "upstream returned invalid OpenAI model-list JSON", + }, + { + name: "missing data", + status: http.StatusOK, + body: `{}`, + wantErr: "upstream returned invalid OpenAI model-list JSON", + }, + { + name: "too many models", + status: http.StatusOK, + body: modelListJSON(maxModels + 1), + wantErr: "upstream returned too many models", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(test.status) + _, _ = io.WriteString(w, test.body) + })) + defer server.Close() + + _, err := newWithTransport(http.DefaultTransport).Discover(context.Background(), Request{ + UpstreamURL: server.URL, + }) + require.EqualError(t, err, test.wantErr) + }) + } +} + +func modelListJSON(count int) string { + var body strings.Builder + body.WriteString(`{"data":[`) + for i := range count { + if i > 0 { + body.WriteByte(',') + } + _, _ = fmt.Fprintf(&body, `{"id":"model-%d"}`, i) + } + body.WriteString(`]}`) + return body.String() +} diff --git a/proxy/management_integration_test.go b/proxy/management_integration_test.go index cb82813b0..9954c5c40 100644 --- a/proxy/management_integration_test.go +++ b/proxy/management_integration_test.go @@ -271,6 +271,10 @@ func (c *testProxyController) SendServiceUpdateToCluster(_ context.Context, _ st // noop } +func (c *testProxyController) DiscoverModels(_ context.Context, _, _ string, _ *proto.ModelDiscoveryRequest) (*proto.ModelDiscoveryResult, error) { + return nil, nbproxy.ErrModelDiscoveryUnavailable +} + func (c *testProxyController) GetOIDCValidationConfig() nbproxy.OIDCValidationConfig { return nbproxy.OIDCValidationConfig{} } diff --git a/proxy/model_discovery_sync_test.go b/proxy/model_discovery_sync_test.go new file mode 100644 index 000000000..90b9aab35 --- /dev/null +++ b/proxy/model_discovery_sync_test.go @@ -0,0 +1,311 @@ +package proxy + +import ( + "context" + "errors" + "fmt" + "io" + "sync/atomic" + "testing" + "time" + + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + + "github.com/netbirdio/netbird/proxy/internal/crowdsec" + "github.com/netbirdio/netbird/proxy/internal/modeldiscovery" + "github.com/netbirdio/netbird/shared/management/proto" +) + +type stubModelDiscoverer struct { + started chan modeldiscovery.Request + release <-chan struct{} + result modeldiscovery.Result + err error +} + +func (s *stubModelDiscoverer) Discover(ctx context.Context, request modeldiscovery.Request) (modeldiscovery.Result, error) { + if s.started != nil { + select { + case s.started <- request: + case <-ctx.Done(): + return modeldiscovery.Result{}, ctx.Err() + } + } + if s.release != nil { + select { + case <-s.release: + case <-ctx.Done(): + return modeldiscovery.Result{}, ctx.Err() + } + } + return s.result, s.err +} + +type modelDiscoverySyncStream struct { + grpc.ClientStream + ctx context.Context + recv chan *proto.SyncMappingsResponse + sent chan *proto.SyncMappingsRequest + sendWait time.Duration + sending atomic.Int32 + overlap atomic.Bool +} + +func newModelDiscoverySyncStream(ctx context.Context) *modelDiscoverySyncStream { + return &modelDiscoverySyncStream{ + ctx: ctx, + recv: make(chan *proto.SyncMappingsResponse, 16), + sent: make(chan *proto.SyncMappingsRequest, 16), + } +} + +func (s *modelDiscoverySyncStream) Send(message *proto.SyncMappingsRequest) error { + if s.sending.Add(1) != 1 { + s.overlap.Store(true) + } + defer s.sending.Add(-1) + if s.sendWait > 0 { + time.Sleep(s.sendWait) + } + select { + case s.sent <- message: + return nil + case <-s.ctx.Done(): + return s.ctx.Err() + } +} + +func (s *modelDiscoverySyncStream) Recv() (*proto.SyncMappingsResponse, error) { + select { + case message, ok := <-s.recv: + if !ok { + return nil, io.EOF + } + return message, nil + case <-s.ctx.Done(): + return nil, s.ctx.Err() + } +} + +func (s *modelDiscoverySyncStream) Context() context.Context { + return s.ctx +} + +func TestProxyCapabilitiesAdvertiseModelDiscovery(t *testing.T) { + t.Parallel() + + server := &Server{ + crowdsecRegistry: crowdsec.NewRegistry("", "", log.New().WithField("test", true)), + } + capabilities := server.proxyCapabilities() + require.NotNil(t, capabilities.SupportsModelDiscovery) + assert.True(t, capabilities.GetSupportsModelDiscovery()) +} + +func TestExecuteModelDiscoveryMapsControlShape(t *testing.T) { + t.Parallel() + + discoverer := &stubModelDiscoverer{ + result: modeldiscovery.Result{ + Source: modeldiscovery.SourceOpenAIV1Models, + Models: []modeldiscovery.Model{ + {ID: "llama3.2:latest", Label: "Llama 3.2"}, + }, + }, + } + request := &proto.ModelDiscoveryRequest{ + RequestId: "request-1", + UpstreamUrl: "http://ollama.internal:11434", + AuthHeaderName: "Authorization", + AuthHeaderValue: "Bearer secret", + SkipTlsVerify: true, + OllamaFallback: true, + } + + result := executeModelDiscovery(context.Background(), discoverer, request) + assert.Equal(t, "request-1", result.GetRequestId()) + assert.Equal(t, modeldiscovery.SourceOpenAIV1Models, result.GetSource()) + require.Len(t, result.GetModels(), 1) + assert.Equal(t, "llama3.2:latest", result.GetModels()[0].GetId()) + assert.Equal(t, "Llama 3.2", result.GetModels()[0].GetLabel()) + + discoverer.started = make(chan modeldiscovery.Request, 1) + _ = executeModelDiscovery(context.Background(), discoverer, request) + received := <-discoverer.started + assert.Equal(t, request.GetUpstreamUrl(), received.UpstreamURL) + assert.Equal(t, request.GetAuthHeaderName(), received.AuthHeaderName) + assert.Equal(t, request.GetAuthHeaderValue(), received.AuthHeaderValue) + assert.True(t, received.SkipTLSVerify) + assert.True(t, received.AllowOllamaFallback) +} + +func TestExecuteModelDiscoveryReturnsSanitizedError(t *testing.T) { + t.Parallel() + + result := executeModelDiscovery(context.Background(), &stubModelDiscoverer{ + err: errors.New("model discovery request failed"), + }, &proto.ModelDiscoveryRequest{RequestId: "request-error"}) + assert.Equal(t, "request-error", result.GetRequestId()) + assert.Equal(t, "model discovery request failed", result.GetError()) + assert.Empty(t, result.GetModels()) + assert.Empty(t, result.GetSource()) +} + +func TestHandleSyncMappingsStreamRunsDiscoveryOutOfBand(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + release := make(chan struct{}) + started := make(chan modeldiscovery.Request, 1) + server := &Server{ + Logger: log.New(), + routerReady: closedChan(), + modelDiscoverer: &stubModelDiscoverer{ + started: started, + release: release, + result: modeldiscovery.Result{ + Source: modeldiscovery.SourceOpenAIV1Models, + Models: []modeldiscovery.Model{{ID: "model-a", Label: "model-a"}}, + }, + }, + } + stream := newModelDiscoverySyncStream(ctx) + stream.sendWait = 10 * time.Millisecond + + done := make(chan error, 1) + initialSyncDone := true + go func() { + done <- server.handleSyncMappingsStream(ctx, stream, &initialSyncDone, time.Time{}) + }() + + stream.recv <- &proto.SyncMappingsResponse{ + ModelDiscoveryRequest: &proto.ModelDiscoveryRequest{ + RequestId: "request-1", + UpstreamUrl: "http://ollama.internal:11434", + }, + } + select { + case <-started: + case <-time.After(time.Second): + t.Fatal("model discovery did not start") + } + + // A normal mapping batch must still be acknowledged while the HTTP probe + // is in flight. + stream.recv <- &proto.SyncMappingsResponse{} + select { + case sent := <-stream.sent: + assert.NotNil(t, sent.GetAck()) + assert.Nil(t, sent.GetModelDiscoveryResult()) + case <-time.After(time.Second): + t.Fatal("mapping ack was blocked by model discovery") + } + + close(release) + select { + case sent := <-stream.sent: + result := sent.GetModelDiscoveryResult() + require.NotNil(t, result) + assert.Equal(t, "request-1", result.GetRequestId()) + assert.Equal(t, modeldiscovery.SourceOpenAIV1Models, result.GetSource()) + assert.Nil(t, sent.GetAck()) + case <-time.After(time.Second): + t.Fatal("model discovery result was not sent") + } + assert.False(t, stream.overlap.Load(), "acks and discovery results must use one serialized sender") + + close(stream.recv) + require.NoError(t, <-done) +} + +func TestHandleSyncMappingsStreamBoundsConcurrentDiscovery(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + release := make(chan struct{}) + started := make(chan modeldiscovery.Request, 8) + server := &Server{ + Logger: log.New(), + routerReady: closedChan(), + modelDiscoverer: &stubModelDiscoverer{ + started: started, + release: release, + }, + } + stream := newModelDiscoverySyncStream(ctx) + + done := make(chan error, 1) + initialSyncDone := true + go func() { + done <- server.handleSyncMappingsStream(ctx, stream, &initialSyncDone, time.Time{}) + }() + + for i := range 5 { + stream.recv <- &proto.SyncMappingsResponse{ + ModelDiscoveryRequest: &proto.ModelDiscoveryRequest{ + RequestId: fmt.Sprintf("request-%d", i), + UpstreamUrl: "http://ollama.internal:11434", + }, + } + } + + for range 4 { + select { + case <-started: + case <-time.After(time.Second): + t.Fatal("expected four concurrent model discoveries") + } + } + select { + case sent := <-stream.sent: + result := sent.GetModelDiscoveryResult() + require.NotNil(t, result) + assert.Equal(t, "model discovery is busy", result.GetError()) + case <-time.After(time.Second): + t.Fatal("fifth discovery did not fail fast") + } + + close(release) + for range 4 { + select { + case sent := <-stream.sent: + require.NotNil(t, sent.GetModelDiscoveryResult()) + case <-time.After(time.Second): + t.Fatal("in-flight model discovery did not complete") + } + } + close(stream.recv) + require.NoError(t, <-done) +} + +func TestHandleSyncMappingsStreamRejectsMixedDiscoveryMessage(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + server := &Server{ + Logger: log.New(), + routerReady: closedChan(), + modelDiscoverer: &stubModelDiscoverer{}, + } + stream := newModelDiscoverySyncStream(ctx) + stream.recv <- &proto.SyncMappingsResponse{ + Mapping: []*proto.ProxyMapping{{Id: "mapping-1"}}, + ModelDiscoveryRequest: &proto.ModelDiscoveryRequest{ + RequestId: "request-1", + }, + } + close(stream.recv) + + initialSyncDone := true + err := server.handleSyncMappingsStream(ctx, stream, &initialSyncDone, time.Time{}) + require.EqualError(t, err, "model discovery message must not include mapping data") +} diff --git a/proxy/server.go b/proxy/server.go index 4f448e4b8..bf54a85b8 100644 --- a/proxy/server.go +++ b/proxy/server.go @@ -57,6 +57,7 @@ import ( proxymetrics "github.com/netbirdio/netbird/proxy/internal/metrics" "github.com/netbirdio/netbird/proxy/internal/middleware" mwbuiltin "github.com/netbirdio/netbird/proxy/internal/middleware/builtin" + "github.com/netbirdio/netbird/proxy/internal/modeldiscovery" "github.com/netbirdio/netbird/proxy/internal/netutil" "github.com/netbirdio/netbird/proxy/internal/proxy" "github.com/netbirdio/netbird/proxy/internal/restrict" @@ -78,6 +79,10 @@ type portRouter struct { cancel context.CancelFunc } +type providerModelDiscoverer interface { + Discover(context.Context, modeldiscovery.Request) (modeldiscovery.Result, error) +} + type Server struct { ctx context.Context mgmtClient proto.ProxyServiceClient @@ -100,16 +105,20 @@ type Server struct { // middlewareRegistry is the source of registered middleware factories. // Concrete middlewares register themselves through init(). middlewareRegistry *middleware.Registry - mainRouter *nbtcp.Router - mainPort uint16 - udpMu sync.Mutex - udpRelays map[types.ServiceID]*udprelay.Relay - udpRelayWg sync.WaitGroup - portMu sync.RWMutex - portRouters map[uint16]*portRouter - svcPorts map[types.ServiceID][]uint16 - lastMappings map[types.ServiceID]*proto.ProxyMapping - portRouterWg sync.WaitGroup + // modelDiscoverer executes explicit provider model-list probes on the + // proxy's host network. Lazily constructed for normal servers; injectable + // in focused control-stream tests. + modelDiscoverer providerModelDiscoverer + mainRouter *nbtcp.Router + mainPort uint16 + udpMu sync.Mutex + udpRelays map[types.ServiceID]*udprelay.Relay + udpRelayWg sync.WaitGroup + portMu sync.RWMutex + portRouters map[uint16]*portRouter + svcPorts map[types.ServiceID][]uint16 + lastMappings map[types.ServiceID]*proto.ProxyMapping + portRouterWg sync.WaitGroup // hijackTracker tracks hijacked connections (e.g. WebSocket upgrades) // so they can be closed during graceful shutdown, since http.Server.Shutdown @@ -1277,12 +1286,16 @@ func (s *Server) proxyCapabilities() *proto.ProxyCapabilities { privateCapability := s.Private // Always true: this build enforces ProxyMapping.private via the auth middleware. supportsPrivateService := true + // Model discovery is handled only on SyncMappings. Management also gates + // dispatch on the live connection using this capability. + supportsModelDiscovery := true return &proto.ProxyCapabilities{ SupportsCustomPorts: &s.SupportsCustomPorts, RequireSubdomain: &s.RequireSubdomain, SupportsCrowdsec: &supportsCrowdSec, Private: &privateCapability, SupportsPrivateService: &supportsPrivateService, + SupportsModelDiscovery: &supportsModelDiscovery, } } @@ -1349,7 +1362,8 @@ func isSyncUnimplemented(err error) bool { // handleSyncMappingsStream consumes batches from a bidirectional SyncMappings // stream, sending an ack after each batch is fully processed. Management waits // for the ack before sending the next batch, providing application-level -// back-pressure. +// back-pressure. Model discovery commands are out-of-band: they run with +// bounded concurrency and return a correlated result instead of an ack. func (s *Server) handleSyncMappingsStream(ctx context.Context, stream proto.ProxyService_SyncMappingsClient, initialSyncDone *bool, connectTime time.Time) error { select { case <-s.routerReady: @@ -1358,6 +1372,29 @@ func (s *Server) handleSyncMappingsStream(ctx context.Context, stream proto.Prox } tracker := s.newSnapshotTracker(initialSyncDone, connectTime) + discoverer := s.modelDiscoverer + if discoverer == nil { + discoverer = modeldiscovery.New(s.Logger) + } + + const maxConcurrentDiscoveries = 4 + discoverySlots := make(chan struct{}, maxConcurrentDiscoveries) + discoveryCtx, cancelDiscoveries := context.WithCancel(ctx) + var discoveryWG sync.WaitGroup + var sendMu sync.Mutex + defer func() { + cancelDiscoveries() + discoveryWG.Wait() + }() + + // gRPC permits one concurrent sender and one concurrent receiver, but not + // multiple senders. Mapping acks and asynchronous discovery results share + // this serialized send path. + send := func(message *proto.SyncMappingsRequest) error { + sendMu.Lock() + defer sendMu.Unlock() + return stream.Send(message) + } for { select { @@ -1372,6 +1409,46 @@ func (s *Server) handleSyncMappingsStream(ctx context.Context, stream proto.Prox return fmt.Errorf("receive msg: %w", err) } + if discovery := msg.GetModelDiscoveryRequest(); discovery != nil { + if len(msg.GetMapping()) != 0 || msg.GetInitialSyncComplete() { + return errors.New("model discovery message must not include mapping data") + } + + select { + case discoverySlots <- struct{}{}: + discoveryWG.Add(1) + go func() { + defer discoveryWG.Done() + defer func() { <-discoverySlots }() + + result := executeModelDiscovery(discoveryCtx, discoverer, discovery) + if discoveryCtx.Err() != nil { + return + } + if err := send(&proto.SyncMappingsRequest{ + Msg: &proto.SyncMappingsRequest_ModelDiscoveryResult{ + ModelDiscoveryResult: result, + }, + }); err != nil { + s.Logger.WithError(err).Debug("failed to send model discovery result") + } + }() + default: + result := &proto.ModelDiscoveryResult{ + RequestId: discovery.GetRequestId(), + Error: "model discovery is busy", + } + if err := send(&proto.SyncMappingsRequest{ + Msg: &proto.SyncMappingsRequest_ModelDiscoveryResult{ + ModelDiscoveryResult: result, + }, + }); err != nil { + return fmt.Errorf("send model discovery busy result: %w", err) + } + } + continue + } + batchStart := time.Now() s.Logger.Debug("Received mapping update, starting processing") if err := s.processMappingsGuarded(ctx, msg.GetMapping()); err != nil { @@ -1380,7 +1457,7 @@ func (s *Server) handleSyncMappingsStream(ctx context.Context, stream proto.Prox s.Logger.Debug("Processing mapping update completed") tracker.recordBatch(ctx, s, msg.GetMapping(), msg.GetInitialSyncComplete(), batchStart) - if err := stream.Send(&proto.SyncMappingsRequest{ + if err := send(&proto.SyncMappingsRequest{ Msg: &proto.SyncMappingsRequest_Ack{Ack: &proto.SyncMappingsAck{}}, }); err != nil { return fmt.Errorf("send ack: %w", err) @@ -1389,6 +1466,31 @@ func (s *Server) handleSyncMappingsStream(ctx context.Context, stream proto.Prox } } +func executeModelDiscovery(ctx context.Context, discoverer providerModelDiscoverer, request *proto.ModelDiscoveryRequest) *proto.ModelDiscoveryResult { + result := &proto.ModelDiscoveryResult{RequestId: request.GetRequestId()} + discovered, err := discoverer.Discover(ctx, modeldiscovery.Request{ + UpstreamURL: request.GetUpstreamUrl(), + AuthHeaderName: request.GetAuthHeaderName(), + AuthHeaderValue: request.GetAuthHeaderValue(), + SkipTLSVerify: request.GetSkipTlsVerify(), + AllowOllamaFallback: request.GetOllamaFallback(), + }) + if err != nil { + result.Error = err.Error() + return result + } + + result.Source = discovered.Source + result.Models = make([]*proto.ModelDiscoveryModel, 0, len(discovered.Models)) + for _, model := range discovered.Models { + result.Models = append(result.Models, &proto.ModelDiscoveryModel{ + Id: model.ID, + Label: model.Label, + }) + } + return result +} + // snapshotTracker accumulates service IDs during the initial snapshot and // finalises sync state when the complete flag arrives. Used by both // handleMappingStream and handleSyncMappingsStream so metric emission and diff --git a/shared/management/http/api/openapi.yml b/shared/management/http/api/openapi.yml index 541e043e9..432f5eb94 100644 --- a/shared/management/http/api/openapi.yml +++ b/shared/management/http/api/openapi.yml @@ -5280,6 +5280,51 @@ components: - id - input_per_1k - output_per_1k + AgentNetworkDiscoveredModel: + type: object + description: A model reported by the provider's persisted upstream endpoint. + properties: + id: + type: string + description: Exact model identifier accepted by the upstream endpoint. + example: "llama3.2:latest" + label: + type: string + description: Human-friendly label reported by the endpoint. Falls back to the model identifier. + example: "llama3.2:latest" + required: + - id + - label + AgentNetworkModelDiscoveryResponse: + type: object + description: | + Result of an explicit model-discovery probe executed by a connected + proxy in the account's selected Agent Network cluster. Discovered + models are not persisted until the provider is updated. + properties: + models: + type: array + description: Normalized, deduplicated models reported by the upstream. + items: + $ref: '#/components/schemas/AgentNetworkDiscoveredModel' + source: + type: string + description: Upstream API shape used to obtain the result. + enum: [openai_v1_models, ollama_api_tags] + example: "openai_v1_models" + proxy_cluster: + type: string + description: Proxy cluster from which the endpoint was queried. + example: "eu.proxy.netbird.io" + request_id: + type: string + description: Correlation identifier for the management-to-proxy probe. + example: "b7ef7da0-78cc-4583-8c4f-55f2ce72015f" + required: + - models + - source + - proxy_cluster + - request_id AgentNetworkCatalogModel: type: object properties: @@ -5356,6 +5401,10 @@ components: "custom" — generic OpenAI-compatible self-hosted endpoint catch-all. enum: [provider, gateway, custom] example: "provider" + supports_model_discovery: + type: boolean + description: Whether models can be loaded from a persisted endpoint through the selected proxy cluster. + example: false extra_headers: type: array description: | @@ -5379,6 +5428,7 @@ components: - default_content_type - brand_color - kind + - supports_model_discovery - models AgentNetworkCatalogIdentityInjection: type: object @@ -14039,6 +14089,48 @@ paths: "$ref": "#/components/responses/not_found" '500': "$ref": "#/components/responses/internal_error" + /api/agent-network/providers/{providerId}/discover-models: + post: + summary: Discover models from an Agent Network provider + description: | + Executes a bounded, read-only model-list probe from a connected proxy + in the account's selected Agent Network cluster. The probe uses the + provider's persisted endpoint, TLS setting, and stored credential. + Returned models are not persisted by this operation. + tags: [ Agent Network ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + parameters: + - in: path + name: providerId + required: true + schema: + type: string + description: The unique identifier of a persisted Agent Network provider + responses: + '200': + description: Models discovered successfully + content: + application/json: + schema: + $ref: '#/components/schemas/AgentNetworkModelDiscoveryResponse' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '404': + "$ref": "#/components/responses/not_found" + '412': + description: The provider cannot be queried or no capable proxy is connected + content: { } + '429': + description: A discovery probe is already running or was requested too recently + content: { } + '500': + "$ref": "#/components/responses/internal_error" /api/agent-network/policies: get: summary: List all Agent Network Policies diff --git a/shared/management/http/api/types.gen.go b/shared/management/http/api/types.gen.go index 0eeed8709..196182471 100644 --- a/shared/management/http/api/types.gen.go +++ b/shared/management/http/api/types.gen.go @@ -98,6 +98,24 @@ func (e AgentNetworkConsumptionDimensionKind) Valid() bool { } } +// Defines values for AgentNetworkModelDiscoveryResponseSource. +const ( + AgentNetworkModelDiscoveryResponseSourceOllamaApiTags AgentNetworkModelDiscoveryResponseSource = "ollama_api_tags" + AgentNetworkModelDiscoveryResponseSourceOpenaiV1Models AgentNetworkModelDiscoveryResponseSource = "openai_v1_models" +) + +// Valid indicates whether the value is a known member of the AgentNetworkModelDiscoveryResponseSource enum. +func (e AgentNetworkModelDiscoveryResponseSource) Valid() bool { + switch e { + case AgentNetworkModelDiscoveryResponseSourceOllamaApiTags: + return true + case AgentNetworkModelDiscoveryResponseSourceOpenaiV1Models: + return true + default: + return false + } +} + // Defines values for CreateAzureIntegrationRequestHost. const ( CreateAzureIntegrationRequestHostMicrosoftCom CreateAzureIntegrationRequestHost = "microsoft.com" @@ -2094,6 +2112,9 @@ type AgentNetworkCatalogProvider struct { // Name Display name for the provider. Name string `json:"name"` + + // SupportsModelDiscovery Whether models can be loaded from a persisted endpoint through the selected proxy cluster. + SupportsModelDiscovery bool `json:"supports_model_discovery"` } // AgentNetworkCatalogProviderAuthMode Whether this provider requires, optionally accepts, or does not support an upstream API key. @@ -2135,6 +2156,15 @@ type AgentNetworkConsumption struct { // AgentNetworkConsumptionDimensionKind Whether this row counts a single end user or a single source group across every member. type AgentNetworkConsumptionDimensionKind string +// AgentNetworkDiscoveredModel A model reported by the provider's persisted upstream endpoint. +type AgentNetworkDiscoveredModel struct { + // Id Exact model identifier accepted by the upstream endpoint. + Id string `json:"id"` + + // Label Human-friendly label reported by the endpoint. Falls back to the model identifier. + Label string `json:"label"` +} + // AgentNetworkGuardrail defines model for AgentNetworkGuardrail. type AgentNetworkGuardrail struct { // Checks Guardrail check parameters. Each entry has an `enabled` flag plus per-check configuration; disabled entries are inert. @@ -2182,6 +2212,26 @@ type AgentNetworkGuardrailRequest struct { Name string `json:"name"` } +// AgentNetworkModelDiscoveryResponse Result of an explicit model-discovery probe executed by a connected +// proxy in the account's selected Agent Network cluster. Discovered +// models are not persisted until the provider is updated. +type AgentNetworkModelDiscoveryResponse struct { + // Models Normalized, deduplicated models reported by the upstream. + Models []AgentNetworkDiscoveredModel `json:"models"` + + // ProxyCluster Proxy cluster from which the endpoint was queried. + ProxyCluster string `json:"proxy_cluster"` + + // RequestId Correlation identifier for the management-to-proxy probe. + RequestId string `json:"request_id"` + + // Source Upstream API shape used to obtain the result. + Source AgentNetworkModelDiscoveryResponseSource `json:"source"` +} + +// AgentNetworkModelDiscoveryResponseSource Upstream API shape used to obtain the result. +type AgentNetworkModelDiscoveryResponseSource string + // AgentNetworkPolicy defines model for AgentNetworkPolicy. type AgentNetworkPolicy struct { // CreatedAt Timestamp when the policy was created. diff --git a/shared/management/proto/proxy_service.pb.go b/shared/management/proto/proxy_service.pb.go index df42d78ff..dabfbccfa 100644 --- a/shared/management/proto/proxy_service.pb.go +++ b/shared/management/proto/proxy_service.pb.go @@ -295,6 +295,10 @@ type ProxyCapabilities struct { Private *bool `protobuf:"varint,4,opt,name=private,proto3,oneof" json:"private,omitempty"` // Whether the proxy enforces ProxyMapping.private (fails closed on ValidateTunnelPeer failure). Management MUST NOT stream private mappings to proxies that don't claim this. SupportsPrivateService *bool `protobuf:"varint,5,opt,name=supports_private_service,json=supportsPrivateService,proto3,oneof" json:"supports_private_service,omitempty"` + // Whether the proxy can execute correlated Agent Network model-discovery + // requests delivered over SyncMappings. Management must not send discovery + // requests to proxies that omit or disable this capability. + SupportsModelDiscovery *bool `protobuf:"varint,6,opt,name=supports_model_discovery,json=supportsModelDiscovery,proto3,oneof" json:"supports_model_discovery,omitempty"` } func (x *ProxyCapabilities) Reset() { @@ -364,6 +368,13 @@ func (x *ProxyCapabilities) GetSupportsPrivateService() bool { return false } +func (x *ProxyCapabilities) GetSupportsModelDiscovery() bool { + if x != nil && x.SupportsModelDiscovery != nil { + return *x.SupportsModelDiscovery + } + return false +} + // GetMappingUpdateRequest is sent to initialise a mapping stream. type GetMappingUpdateRequest struct { state protoimpl.MessageState @@ -2581,6 +2592,7 @@ type SyncMappingsRequest struct { // // *SyncMappingsRequest_Init // *SyncMappingsRequest_Ack + // *SyncMappingsRequest_ModelDiscoveryResult Msg isSyncMappingsRequest_Msg `protobuf_oneof:"msg"` } @@ -2637,6 +2649,13 @@ func (x *SyncMappingsRequest) GetAck() *SyncMappingsAck { return nil } +func (x *SyncMappingsRequest) GetModelDiscoveryResult() *ModelDiscoveryResult { + if x, ok := x.GetMsg().(*SyncMappingsRequest_ModelDiscoveryResult); ok { + return x.ModelDiscoveryResult + } + return nil +} + type isSyncMappingsRequest_Msg interface { isSyncMappingsRequest_Msg() } @@ -2649,10 +2668,17 @@ type SyncMappingsRequest_Ack struct { Ack *SyncMappingsAck `protobuf:"bytes,2,opt,name=ack,proto3,oneof"` } +type SyncMappingsRequest_ModelDiscoveryResult struct { + // Correlated response to a ModelDiscoveryRequest received from management. + ModelDiscoveryResult *ModelDiscoveryResult `protobuf:"bytes,3,opt,name=model_discovery_result,json=modelDiscoveryResult,proto3,oneof"` +} + func (*SyncMappingsRequest_Init) isSyncMappingsRequest_Msg() {} func (*SyncMappingsRequest_Ack) isSyncMappingsRequest_Msg() {} +func (*SyncMappingsRequest_ModelDiscoveryResult) isSyncMappingsRequest_Msg() {} + // SyncMappingsInit is the first message on the stream, carrying the same // identification fields as GetMappingUpdateRequest. type SyncMappingsInit struct { @@ -2784,6 +2810,9 @@ type SyncMappingsResponse struct { Mapping []*ProxyMapping `protobuf:"bytes,1,rep,name=mapping,proto3" json:"mapping,omitempty"` // initial_sync_complete is set on the last message of the initial snapshot. InitialSyncComplete bool `protobuf:"varint,2,opt,name=initial_sync_complete,json=initialSyncComplete,proto3" json:"initial_sync_complete,omitempty"` + // An out-of-band model-discovery operation. This is sent only after the + // initial snapshot and only to proxies advertising supports_model_discovery. + ModelDiscoveryRequest *ModelDiscoveryRequest `protobuf:"bytes,3,opt,name=model_discovery_request,json=modelDiscoveryRequest,proto3" json:"model_discovery_request,omitempty"` } func (x *SyncMappingsResponse) Reset() { @@ -2832,6 +2861,13 @@ func (x *SyncMappingsResponse) GetInitialSyncComplete() bool { return false } +func (x *SyncMappingsResponse) GetModelDiscoveryRequest() *ModelDiscoveryRequest { + if x != nil { + return x.ModelDiscoveryRequest + } + return nil +} + // CheckLLMPolicyLimitsRequest carries the resolved caller identity and the // upstream provider already chosen by llm_router. Management computes which // policies authorise the request, picks the one with the most remaining @@ -3164,6 +3200,229 @@ func (*RecordLLMUsageResponse) Descriptor() ([]byte, []int) { return file_proxy_service_proto_rawDescGZIP(), []int{36} } +// ModelDiscoveryRequest asks a proxy to list models from a persisted provider +// endpoint using the proxy host's network path. request_id is assigned by +// management and echoed verbatim in ModelDiscoveryResult. +type ModelDiscoveryRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + UpstreamUrl string `protobuf:"bytes,2,opt,name=upstream_url,json=upstreamUrl,proto3" json:"upstream_url,omitempty"` + AuthHeaderName string `protobuf:"bytes,3,opt,name=auth_header_name,json=authHeaderName,proto3" json:"auth_header_name,omitempty"` + AuthHeaderValue string `protobuf:"bytes,4,opt,name=auth_header_value,json=authHeaderValue,proto3" json:"auth_header_value,omitempty"` + SkipTlsVerify bool `protobuf:"varint,5,opt,name=skip_tls_verify,json=skipTlsVerify,proto3" json:"skip_tls_verify,omitempty"` + // When true, the proxy may fall back from the OpenAI-compatible + // GET /v1/models endpoint to Ollama's native GET /api/tags endpoint. + OllamaFallback bool `protobuf:"varint,6,opt,name=ollama_fallback,json=ollamaFallback,proto3" json:"ollama_fallback,omitempty"` +} + +func (x *ModelDiscoveryRequest) Reset() { + *x = ModelDiscoveryRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_service_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ModelDiscoveryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModelDiscoveryRequest) ProtoMessage() {} + +func (x *ModelDiscoveryRequest) ProtoReflect() protoreflect.Message { + mi := &file_proxy_service_proto_msgTypes[37] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ModelDiscoveryRequest.ProtoReflect.Descriptor instead. +func (*ModelDiscoveryRequest) Descriptor() ([]byte, []int) { + return file_proxy_service_proto_rawDescGZIP(), []int{37} +} + +func (x *ModelDiscoveryRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *ModelDiscoveryRequest) GetUpstreamUrl() string { + if x != nil { + return x.UpstreamUrl + } + return "" +} + +func (x *ModelDiscoveryRequest) GetAuthHeaderName() string { + if x != nil { + return x.AuthHeaderName + } + return "" +} + +func (x *ModelDiscoveryRequest) GetAuthHeaderValue() string { + if x != nil { + return x.AuthHeaderValue + } + return "" +} + +func (x *ModelDiscoveryRequest) GetSkipTlsVerify() bool { + if x != nil { + return x.SkipTlsVerify + } + return false +} + +func (x *ModelDiscoveryRequest) GetOllamaFallback() bool { + if x != nil { + return x.OllamaFallback + } + return false +} + +// ModelDiscoveryModel is the normalized model shape returned to management. +// Arbitrary upstream fields never cross the proxy control channel. +type ModelDiscoveryModel struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"` +} + +func (x *ModelDiscoveryModel) Reset() { + *x = ModelDiscoveryModel{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_service_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ModelDiscoveryModel) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModelDiscoveryModel) ProtoMessage() {} + +func (x *ModelDiscoveryModel) ProtoReflect() protoreflect.Message { + mi := &file_proxy_service_proto_msgTypes[38] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ModelDiscoveryModel.ProtoReflect.Descriptor instead. +func (*ModelDiscoveryModel) Descriptor() ([]byte, []int) { + return file_proxy_service_proto_rawDescGZIP(), []int{38} +} + +func (x *ModelDiscoveryModel) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *ModelDiscoveryModel) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + +// ModelDiscoveryResult completes one ModelDiscoveryRequest. error is empty on +// success and contains only a sanitized diagnostic on failure. +type ModelDiscoveryResult struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + Models []*ModelDiscoveryModel `protobuf:"bytes,2,rep,name=models,proto3" json:"models,omitempty"` + // Stable source label, currently openai_v1_models or ollama_api_tags. + Source string `protobuf:"bytes,3,opt,name=source,proto3" json:"source,omitempty"` + Error string `protobuf:"bytes,4,opt,name=error,proto3" json:"error,omitempty"` +} + +func (x *ModelDiscoveryResult) Reset() { + *x = ModelDiscoveryResult{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_service_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ModelDiscoveryResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModelDiscoveryResult) ProtoMessage() {} + +func (x *ModelDiscoveryResult) ProtoReflect() protoreflect.Message { + mi := &file_proxy_service_proto_msgTypes[39] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ModelDiscoveryResult.ProtoReflect.Descriptor instead. +func (*ModelDiscoveryResult) Descriptor() ([]byte, []int) { + return file_proxy_service_proto_rawDescGZIP(), []int{39} +} + +func (x *ModelDiscoveryResult) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *ModelDiscoveryResult) GetModels() []*ModelDiscoveryModel { + if x != nil { + return x.Models + } + return nil +} + +func (x *ModelDiscoveryResult) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *ModelDiscoveryResult) GetError() string { + if x != nil { + return x.Error + } + return "" +} + var File_proxy_service_proto protoreflect.FileDescriptor var file_proxy_service_proto_rawDesc = []byte{ @@ -3173,7 +3432,7 @@ var file_proxy_service_proto_rawDesc = []byte{ 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x22, 0xfd, 0x02, 0x0a, 0x11, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x43, 0x61, 0x70, 0x61, + 0x74, 0x6f, 0x22, 0xd9, 0x03, 0x0a, 0x11, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x37, 0x0a, 0x15, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x5f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x13, 0x73, 0x75, 0x70, 0x70, 0x6f, @@ -3190,327 +3449,352 @@ var file_proxy_service_proto_rawDesc = []byte{ 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x48, 0x04, 0x52, 0x16, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x88, 0x01, 0x01, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x73, - 0x5f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x42, 0x14, 0x0a, - 0x12, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x5f, 0x73, 0x75, 0x62, 0x64, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x42, 0x14, 0x0a, 0x12, 0x5f, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x73, - 0x5f, 0x63, 0x72, 0x6f, 0x77, 0x64, 0x73, 0x65, 0x63, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x70, 0x72, - 0x69, 0x76, 0x61, 0x74, 0x65, 0x42, 0x1b, 0x0a, 0x19, 0x5f, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, - 0x74, 0x73, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x22, 0xe6, 0x01, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, - 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, - 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x07, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x12, 0x39, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x61, - 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x18, - 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x41, 0x0a, 0x0c, 0x63, 0x61, 0x70, 0x61, - 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x78, - 0x79, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x0c, 0x63, - 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x22, 0x82, 0x01, 0x0a, 0x18, - 0x47, 0x65, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x07, 0x6d, 0x61, 0x70, 0x70, - 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x61, 0x70, 0x70, - 0x69, 0x6e, 0x67, 0x52, 0x07, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x32, 0x0a, 0x15, - 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x73, 0x79, 0x6e, 0x63, 0x5f, 0x63, 0x6f, 0x6d, - 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x69, 0x6e, 0x69, - 0x74, 0x69, 0x61, 0x6c, 0x53, 0x79, 0x6e, 0x63, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, - 0x22, 0xb6, 0x06, 0x0a, 0x11, 0x50, 0x61, 0x74, 0x68, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x4f, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x73, 0x6b, 0x69, 0x70, 0x5f, 0x74, - 0x6c, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0d, 0x73, 0x6b, 0x69, 0x70, 0x54, 0x6c, 0x73, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x12, 0x42, - 0x0a, 0x0f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, - 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x0e, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x6f, - 0x75, 0x74, 0x12, 0x3e, 0x0a, 0x0c, 0x70, 0x61, 0x74, 0x68, 0x5f, 0x72, 0x65, 0x77, 0x72, 0x69, - 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x52, 0x65, 0x77, 0x72, 0x69, 0x74, - 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x0b, 0x70, 0x61, 0x74, 0x68, 0x52, 0x65, 0x77, 0x72, 0x69, - 0x74, 0x65, 0x12, 0x57, 0x0a, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x68, 0x65, 0x61, - 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x54, 0x61, 0x72, 0x67, - 0x65, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, - 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0d, 0x63, 0x75, - 0x73, 0x74, 0x6f, 0x6d, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x70, - 0x72, 0x6f, 0x78, 0x79, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x0d, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, - 0x6f, 0x6c, 0x12, 0x4b, 0x0a, 0x14, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, - 0x6c, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x12, 0x73, 0x65, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x6c, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, - 0x27, 0x0a, 0x0f, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x70, 0x73, 0x74, 0x72, 0x65, - 0x61, 0x6d, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, - 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x39, 0x0a, 0x19, 0x63, 0x61, 0x70, 0x74, - 0x75, 0x72, 0x65, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, - 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x16, 0x63, 0x61, 0x70, - 0x74, 0x75, 0x72, 0x65, 0x4d, 0x61, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x79, - 0x74, 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x1a, 0x63, 0x61, 0x70, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x6d, - 0x61, 0x78, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x62, 0x79, 0x74, 0x65, - 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x17, 0x63, 0x61, 0x70, 0x74, 0x75, 0x72, 0x65, - 0x4d, 0x61, 0x78, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x79, 0x74, 0x65, 0x73, - 0x12, 0x32, 0x0a, 0x15, 0x63, 0x61, 0x70, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, - 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x13, 0x63, 0x61, 0x70, 0x74, 0x75, 0x72, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, - 0x79, 0x70, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x0b, 0x6d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, - 0x72, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, 0x72, - 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x6d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, - 0x61, 0x72, 0x65, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x6e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x2c, 0x0a, 0x12, 0x64, 0x69, 0x73, - 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6c, 0x6f, 0x67, 0x18, - 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x63, - 0x63, 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x67, 0x1a, 0x40, 0x0a, 0x12, 0x43, 0x75, 0x73, 0x74, 0x6f, - 0x6d, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, - 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, - 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xd1, 0x02, 0x0a, 0x10, 0x4d, 0x69, - 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, 0x72, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x0e, - 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18, - 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x2e, 0x0a, 0x04, 0x73, 0x6c, 0x6f, 0x74, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, 0x72, 0x65, 0x53, 0x6c, - 0x6f, 0x74, 0x52, 0x04, 0x73, 0x6c, 0x6f, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4a, 0x73, 0x6f, 0x6e, 0x12, 0x42, 0x0a, 0x09, 0x66, 0x61, 0x69, - 0x6c, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x25, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x69, 0x64, 0x64, 0x6c, 0x65, - 0x77, 0x61, 0x72, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x4d, - 0x6f, 0x64, 0x65, 0x52, 0x08, 0x66, 0x61, 0x69, 0x6c, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x33, 0x0a, - 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, - 0x75, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x61, 0x6e, 0x5f, 0x6d, 0x75, 0x74, 0x61, 0x74, 0x65, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x61, 0x6e, 0x4d, 0x75, 0x74, 0x61, 0x74, - 0x65, 0x22, 0x2a, 0x0a, 0x08, 0x46, 0x61, 0x69, 0x6c, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x0d, 0x0a, - 0x09, 0x46, 0x41, 0x49, 0x4c, 0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, - 0x46, 0x41, 0x49, 0x4c, 0x5f, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x44, 0x10, 0x01, 0x22, 0x72, 0x0a, - 0x0b, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x12, 0x0a, 0x04, - 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, - 0x12, 0x16, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x37, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x54, 0x61, 0x72, 0x67, 0x65, - 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x22, 0x47, 0x0a, 0x0a, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x12, - 0x16, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x68, 0x61, 0x73, 0x68, 0x65, - 0x64, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x68, - 0x61, 0x73, 0x68, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xe5, 0x01, 0x0a, 0x0e, 0x41, - 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, - 0x0b, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x12, 0x35, - 0x0a, 0x17, 0x6d, 0x61, 0x78, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x67, - 0x65, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x14, 0x6d, 0x61, 0x78, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x41, 0x67, 0x65, 0x53, 0x65, - 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, - 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, - 0x64, 0x12, 0x10, 0x0a, 0x03, 0x70, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, - 0x70, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6f, 0x69, 0x64, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x04, 0x6f, 0x69, 0x64, 0x63, 0x12, 0x39, 0x0a, 0x0c, 0x68, 0x65, 0x61, 0x64, 0x65, - 0x72, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, - 0x72, 0x41, 0x75, 0x74, 0x68, 0x52, 0x0b, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, - 0x68, 0x73, 0x22, 0xdd, 0x01, 0x0a, 0x12, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, - 0x74, 0x72, 0x69, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x6c, 0x6c, - 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x0c, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x43, 0x69, 0x64, 0x72, 0x73, 0x12, 0x23, - 0x0a, 0x0d, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x73, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x43, 0x69, - 0x64, 0x72, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x63, - 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, - 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, - 0x12, 0x2b, 0x0a, 0x11, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x62, 0x6c, 0x6f, - 0x63, 0x6b, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x12, 0x23, 0x0a, - 0x0d, 0x63, 0x72, 0x6f, 0x77, 0x64, 0x73, 0x65, 0x63, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x72, 0x6f, 0x77, 0x64, 0x73, 0x65, 0x63, 0x4d, 0x6f, - 0x64, 0x65, 0x22, 0x80, 0x04, 0x0a, 0x0c, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x61, 0x70, 0x70, - 0x69, 0x6e, 0x67, 0x12, 0x36, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x22, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, - 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, - 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x61, - 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x09, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, - 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x12, 0x2b, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x61, - 0x74, 0x68, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, - 0x1d, 0x0a, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x2e, - 0x0a, 0x04, 0x61, 0x75, 0x74, 0x68, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, - 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x04, 0x61, 0x75, 0x74, 0x68, 0x12, 0x28, - 0x0a, 0x10, 0x70, 0x61, 0x73, 0x73, 0x5f, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x68, 0x65, 0x61, 0x64, - 0x65, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x70, 0x61, 0x73, 0x73, 0x48, 0x6f, - 0x73, 0x74, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x2b, 0x0a, 0x11, 0x72, 0x65, 0x77, 0x72, - 0x69, 0x74, 0x65, 0x5f, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x73, 0x18, 0x09, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x65, 0x77, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x64, 0x69, - 0x72, 0x65, 0x63, 0x74, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x0a, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x6c, 0x69, 0x73, - 0x74, 0x65, 0x6e, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, - 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x4f, 0x0a, 0x13, 0x61, 0x63, - 0x63, 0x65, 0x73, 0x73, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x72, 0x69, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x74, 0x72, - 0x69, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x12, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, - 0x65, 0x73, 0x74, 0x72, 0x69, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x70, - 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x70, 0x72, - 0x69, 0x76, 0x61, 0x74, 0x65, 0x22, 0x3f, 0x0a, 0x14, 0x53, 0x65, 0x6e, 0x64, 0x41, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, - 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x6f, - 0x67, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x22, 0x17, 0x0a, 0x15, 0x53, 0x65, 0x6e, 0x64, 0x41, 0x63, - 0x63, 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0xa9, 0x05, 0x0a, 0x09, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x67, 0x12, 0x38, 0x0a, - 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x15, 0x0a, 0x06, 0x6c, 0x6f, 0x67, 0x5f, 0x69, - 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x6f, 0x67, 0x49, 0x64, 0x12, 0x1d, - 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, - 0x0a, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, - 0x68, 0x6f, 0x73, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, 0x73, 0x74, - 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x70, 0x61, 0x74, 0x68, 0x12, 0x1f, 0x0a, 0x0b, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x5f, 0x6d, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x64, 0x75, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x4d, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, - 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x23, 0x0a, - 0x0d, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x09, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x6f, - 0x64, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x70, 0x18, - 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x70, 0x12, - 0x25, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x63, 0x68, 0x61, 0x6e, 0x69, 0x73, - 0x6d, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x63, - 0x68, 0x61, 0x6e, 0x69, 0x73, 0x6d, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, - 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, - 0x21, 0x0a, 0x0c, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, - 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x61, 0x75, 0x74, 0x68, 0x53, 0x75, 0x63, 0x63, 0x65, - 0x73, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x75, 0x70, 0x6c, 0x6f, - 0x61, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x62, 0x79, 0x74, 0x65, 0x73, 0x55, - 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x64, - 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x62, - 0x79, 0x74, 0x65, 0x73, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x1a, 0x0a, 0x08, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x3f, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x18, 0x11, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x6f, - 0x67, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, - 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x12, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x1a, 0x3b, - 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, - 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, - 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xf8, 0x01, 0x0a, 0x13, - 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x02, 0x69, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, - 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x49, 0x64, 0x12, 0x39, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x48, 0x00, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x2a, 0x0a, - 0x03, 0x70, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x48, 0x00, 0x52, 0x03, 0x70, 0x69, 0x6e, 0x12, 0x40, 0x0a, 0x0b, 0x68, 0x65, 0x61, - 0x64, 0x65, 0x72, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x64, - 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, - 0x0a, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x42, 0x09, 0x0a, 0x07, 0x72, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x57, 0x0a, 0x11, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, - 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x68, - 0x65, 0x61, 0x64, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0b, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1f, - 0x0a, 0x0b, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0a, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x22, - 0x2d, 0x0a, 0x0f, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x1e, - 0x0a, 0x0a, 0x50, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, - 0x70, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x70, 0x69, 0x6e, 0x22, 0x55, - 0x0a, 0x14, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, - 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, - 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xda, 0x02, 0x0a, 0x17, 0x53, 0x65, 0x6e, 0x64, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, - 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, - 0x2f, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, - 0x78, 0x79, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x12, 0x2d, 0x0a, 0x12, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, - 0x69, 0x73, 0x73, 0x75, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x63, 0x65, - 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x49, 0x73, 0x73, 0x75, 0x65, 0x64, 0x12, - 0x28, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x88, 0x01, 0x01, 0x12, 0x50, 0x0a, 0x10, 0x69, 0x6e, 0x62, - 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x18, 0x32, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x4c, 0x69, 0x73, - 0x74, 0x65, 0x6e, 0x65, 0x72, 0x48, 0x01, 0x52, 0x0f, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, - 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x88, 0x01, 0x01, 0x42, 0x10, 0x0a, 0x0e, 0x5f, - 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x42, 0x13, 0x0a, - 0x11, 0x5f, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, - 0x65, 0x72, 0x22, 0x6f, 0x0a, 0x14, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x49, 0x6e, 0x62, 0x6f, 0x75, - 0x6e, 0x64, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x75, - 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, - 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x70, 0x12, 0x1d, 0x0a, 0x0a, 0x68, 0x74, 0x74, 0x70, 0x73, - 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x68, 0x74, 0x74, - 0x70, 0x73, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x70, - 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x68, 0x74, 0x74, 0x70, 0x50, - 0x6f, 0x72, 0x74, 0x22, 0x1a, 0x0a, 0x18, 0x53, 0x65, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0xb8, 0x01, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x50, - 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, - 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x63, - 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, - 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, - 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x30, - 0x0a, 0x14, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x5f, 0x70, 0x75, 0x62, 0x6c, - 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x77, 0x69, - 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, - 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x07, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x22, 0x6f, 0x0a, 0x17, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, - 0x28, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x88, 0x01, 0x01, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x65, 0x0a, 0x11, 0x47, - 0x65, 0x74, 0x4f, 0x49, 0x44, 0x43, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, - 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, - 0x21, 0x0a, 0x0c, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x72, 0x6c, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, - 0x72, 0x6c, 0x22, 0x26, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4f, 0x49, 0x44, 0x43, 0x55, 0x52, 0x4c, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x22, 0x55, 0x0a, 0x16, 0x56, 0x61, - 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x23, 0x0a, 0x0d, + 0x88, 0x01, 0x01, 0x12, 0x3d, 0x0a, 0x18, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x5f, + 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x08, 0x48, 0x05, 0x52, 0x16, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, + 0x73, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x88, + 0x01, 0x01, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x5f, + 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x42, 0x14, 0x0a, 0x12, + 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x5f, 0x73, 0x75, 0x62, 0x64, 0x6f, 0x6d, 0x61, + 0x69, 0x6e, 0x42, 0x14, 0x0a, 0x12, 0x5f, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x5f, + 0x63, 0x72, 0x6f, 0x77, 0x64, 0x73, 0x65, 0x63, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x70, 0x72, 0x69, + 0x76, 0x61, 0x74, 0x65, 0x42, 0x1b, 0x0a, 0x19, 0x5f, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, + 0x73, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x42, 0x1b, 0x0a, 0x19, 0x5f, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x5f, 0x6d, + 0x6f, 0x64, 0x65, 0x6c, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x22, 0xe6, + 0x01, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x70, 0x72, + 0x6f, 0x78, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, + 0x6f, 0x78, 0x79, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, + 0x39, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x12, 0x41, 0x0a, 0x0c, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, + 0x74, 0x69, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x43, 0x61, 0x70, + 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x0c, 0x63, 0x61, 0x70, 0x61, 0x62, + 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x22, 0x82, 0x01, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x4d, + 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x07, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, + 0x07, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x32, 0x0a, 0x15, 0x69, 0x6e, 0x69, 0x74, + 0x69, 0x61, 0x6c, 0x5f, 0x73, 0x79, 0x6e, 0x63, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, + 0x53, 0x79, 0x6e, 0x63, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x22, 0xb6, 0x06, 0x0a, + 0x11, 0x50, 0x61, 0x74, 0x68, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x73, 0x6b, 0x69, 0x70, 0x5f, 0x74, 0x6c, 0x73, 0x5f, 0x76, + 0x65, 0x72, 0x69, 0x66, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x73, 0x6b, 0x69, + 0x70, 0x54, 0x6c, 0x73, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x12, 0x42, 0x0a, 0x0f, 0x72, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0e, + 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x3e, + 0x0a, 0x0c, 0x70, 0x61, 0x74, 0x68, 0x5f, 0x72, 0x65, 0x77, 0x72, 0x69, 0x74, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x52, 0x65, 0x77, 0x72, 0x69, 0x74, 0x65, 0x4d, 0x6f, 0x64, + 0x65, 0x52, 0x0b, 0x70, 0x61, 0x74, 0x68, 0x52, 0x65, 0x77, 0x72, 0x69, 0x74, 0x65, 0x12, 0x57, + 0x0a, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, + 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x4f, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x48, 0x65, 0x61, 0x64, + 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0d, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, + 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x78, 0x79, + 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0d, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x4b, + 0x0a, 0x14, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x6c, 0x65, 0x5f, 0x74, + 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, + 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x12, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x49, 0x64, 0x6c, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x64, + 0x69, 0x72, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x70, 0x73, 0x74, + 0x72, 0x65, 0x61, 0x6d, 0x12, 0x39, 0x0a, 0x19, 0x63, 0x61, 0x70, 0x74, 0x75, 0x72, 0x65, 0x5f, + 0x6d, 0x61, 0x78, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x62, 0x79, 0x74, 0x65, + 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x16, 0x63, 0x61, 0x70, 0x74, 0x75, 0x72, 0x65, + 0x4d, 0x61, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, + 0x3b, 0x0a, 0x1a, 0x63, 0x61, 0x70, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x72, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x17, 0x63, 0x61, 0x70, 0x74, 0x75, 0x72, 0x65, 0x4d, 0x61, 0x78, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x15, + 0x63, 0x61, 0x70, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, + 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x13, 0x63, 0x61, 0x70, + 0x74, 0x75, 0x72, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x73, + 0x12, 0x3e, 0x0a, 0x0b, 0x6d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, 0x72, 0x65, 0x73, 0x18, + 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x4d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, 0x72, 0x65, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x0b, 0x6d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, 0x72, 0x65, 0x73, + 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x4e, 0x65, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x2c, 0x0a, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, + 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6c, 0x6f, 0x67, 0x18, 0x0d, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x10, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x4c, 0x6f, 0x67, 0x1a, 0x40, 0x0a, 0x12, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x48, 0x65, 0x61, + 0x64, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xd1, 0x02, 0x0a, 0x10, 0x4d, 0x69, 0x64, 0x64, 0x6c, 0x65, + 0x77, 0x61, 0x72, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, + 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, + 0x62, 0x6c, 0x65, 0x64, 0x12, 0x2e, 0x0a, 0x04, 0x73, 0x6c, 0x6f, 0x74, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x4d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, 0x72, 0x65, 0x53, 0x6c, 0x6f, 0x74, 0x52, 0x04, + 0x73, 0x6c, 0x6f, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x6a, + 0x73, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x4a, 0x73, 0x6f, 0x6e, 0x12, 0x42, 0x0a, 0x09, 0x66, 0x61, 0x69, 0x6c, 0x5f, 0x6d, 0x6f, + 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x25, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, 0x72, 0x65, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x4d, 0x6f, 0x64, 0x65, 0x52, + 0x08, 0x66, 0x61, 0x69, 0x6c, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x74, 0x69, 0x6d, + 0x65, 0x6f, 0x75, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x1d, + 0x0a, 0x0a, 0x63, 0x61, 0x6e, 0x5f, 0x6d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x09, 0x63, 0x61, 0x6e, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x22, 0x2a, 0x0a, + 0x08, 0x46, 0x61, 0x69, 0x6c, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x0d, 0x0a, 0x09, 0x46, 0x41, 0x49, + 0x4c, 0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x46, 0x41, 0x49, 0x4c, + 0x5f, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x44, 0x10, 0x01, 0x22, 0x72, 0x0a, 0x0b, 0x50, 0x61, 0x74, + 0x68, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, + 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, + 0x72, 0x67, 0x65, 0x74, 0x12, 0x37, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x4f, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x47, 0x0a, + 0x0a, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x68, + 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x68, 0x65, 0x61, + 0x64, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x68, 0x61, 0x73, 0x68, 0x65, 0x64, 0x5f, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x68, 0x61, 0x73, 0x68, 0x65, + 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xe5, 0x01, 0x0a, 0x0e, 0x41, 0x75, 0x74, 0x68, 0x65, + 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, + 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x12, 0x35, 0x0a, 0x17, 0x6d, 0x61, + 0x78, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x65, + 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x14, 0x6d, 0x61, 0x78, + 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x41, 0x67, 0x65, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, + 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x10, 0x0a, + 0x03, 0x70, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x70, 0x69, 0x6e, 0x12, + 0x12, 0x0a, 0x04, 0x6f, 0x69, 0x64, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x6f, + 0x69, 0x64, 0x63, 0x12, 0x39, 0x0a, 0x0c, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x5f, 0x61, 0x75, + 0x74, 0x68, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, + 0x68, 0x52, 0x0b, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x73, 0x22, 0xdd, + 0x01, 0x0a, 0x12, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x74, 0x72, 0x69, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, + 0x5f, 0x63, 0x69, 0x64, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x6c, + 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x43, 0x69, 0x64, 0x72, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x62, 0x6c, + 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x43, 0x69, 0x64, 0x72, 0x73, 0x12, + 0x2b, 0x0a, 0x11, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x72, 0x69, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x61, 0x6c, 0x6c, 0x6f, + 0x77, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x11, + 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, + 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x72, 0x6f, + 0x77, 0x64, 0x73, 0x65, 0x63, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x63, 0x72, 0x6f, 0x77, 0x64, 0x73, 0x65, 0x63, 0x4d, 0x6f, 0x64, 0x65, 0x22, 0x80, + 0x04, 0x0a, 0x0c, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, + 0x36, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, + 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, + 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x2b, + 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, + 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x1d, 0x0a, 0x0a, 0x61, + 0x75, 0x74, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x61, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x2e, 0x0a, 0x04, 0x61, 0x75, + 0x74, 0x68, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x04, 0x61, 0x75, 0x74, 0x68, 0x12, 0x28, 0x0a, 0x10, 0x70, 0x61, + 0x73, 0x73, 0x5f, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x08, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x70, 0x61, 0x73, 0x73, 0x48, 0x6f, 0x73, 0x74, 0x48, 0x65, + 0x61, 0x64, 0x65, 0x72, 0x12, 0x2b, 0x0a, 0x11, 0x72, 0x65, 0x77, 0x72, 0x69, 0x74, 0x65, 0x5f, + 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x10, 0x72, 0x65, 0x77, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, + 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x5f, + 0x70, 0x6f, 0x72, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x6c, 0x69, 0x73, 0x74, + 0x65, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x4f, 0x0a, 0x13, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x5f, 0x72, 0x65, 0x73, 0x74, 0x72, 0x69, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0c, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x74, 0x72, 0x69, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x52, 0x12, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x74, 0x72, + 0x69, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, + 0x65, 0x22, 0x3f, 0x0a, 0x14, 0x53, 0x65, 0x6e, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, + 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6c, 0x6f, 0x67, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x67, 0x52, 0x03, 0x6c, + 0x6f, 0x67, 0x22, 0x17, 0x0a, 0x15, 0x53, 0x65, 0x6e, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xa9, 0x05, 0x0a, 0x09, + 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x67, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x12, 0x15, 0x0a, 0x06, 0x6c, 0x6f, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x6f, 0x67, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, + 0x70, 0x61, 0x74, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, + 0x12, 0x1f, 0x0a, 0x0b, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x73, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, + 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x0c, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1b, + 0x0a, 0x09, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x70, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x61, + 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x63, 0x68, 0x61, 0x6e, 0x69, 0x73, 0x6d, 0x18, 0x0b, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x63, 0x68, 0x61, 0x6e, 0x69, + 0x73, 0x6d, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x61, + 0x75, 0x74, 0x68, 0x5f, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x0b, 0x61, 0x75, 0x74, 0x68, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x21, + 0x0a, 0x0c, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x0e, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x62, 0x79, 0x74, 0x65, 0x73, 0x55, 0x70, 0x6c, 0x6f, 0x61, + 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x64, 0x6f, 0x77, 0x6e, 0x6c, + 0x6f, 0x61, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x62, 0x79, 0x74, 0x65, 0x73, + 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x3f, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x18, 0x11, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x67, 0x2e, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x6e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x12, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, + 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xf8, 0x01, 0x0a, 0x13, 0x41, 0x75, 0x74, 0x68, + 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, + 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x39, + 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x61, + 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, + 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x2a, 0x0a, 0x03, 0x70, 0x69, 0x6e, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, + 0x52, 0x03, 0x70, 0x69, 0x6e, 0x12, 0x40, 0x0a, 0x0b, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x5f, + 0x61, 0x75, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x41, 0x75, + 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0a, 0x68, 0x65, 0x61, + 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x42, 0x09, 0x0a, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x22, 0x57, 0x0a, 0x11, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x68, 0x65, 0x61, 0x64, 0x65, + 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x68, + 0x65, 0x61, 0x64, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x68, 0x65, + 0x61, 0x64, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0a, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x2d, 0x0a, 0x0f, 0x50, + 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, + 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x1e, 0x0a, 0x0a, 0x50, 0x69, + 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x70, 0x69, 0x6e, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x70, 0x69, 0x6e, 0x22, 0x55, 0x0a, 0x14, 0x41, 0x75, + 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x22, 0xdc, 0x01, 0x0a, 0x17, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, - 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, + 0x6e, 0x22, 0xda, 0x02, 0x0a, 0x17, 0x53, 0x65, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, + 0x0a, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, + 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x2d, 0x0a, 0x12, + 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x69, 0x73, 0x73, 0x75, + 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x49, 0x73, 0x73, 0x75, 0x65, 0x64, 0x12, 0x28, 0x0a, 0x0d, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x09, 0x48, 0x00, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x88, 0x01, 0x01, 0x12, 0x50, 0x0a, 0x10, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, + 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x18, 0x32, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x20, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, + 0x78, 0x79, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, + 0x72, 0x48, 0x01, 0x52, 0x0f, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x4c, 0x69, 0x73, 0x74, + 0x65, 0x6e, 0x65, 0x72, 0x88, 0x01, 0x01, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x69, 0x6e, + 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x22, 0x6f, + 0x0a, 0x14, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x4c, 0x69, + 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, + 0x5f, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x75, 0x6e, 0x6e, 0x65, + 0x6c, 0x49, 0x70, 0x12, 0x1d, 0x0a, 0x0a, 0x68, 0x74, 0x74, 0x70, 0x73, 0x5f, 0x70, 0x6f, 0x72, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x68, 0x74, 0x74, 0x70, 0x73, 0x50, 0x6f, + 0x72, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x68, 0x74, 0x74, 0x70, 0x50, 0x6f, 0x72, 0x74, 0x22, + 0x1a, 0x0a, 0x18, 0x53, 0x65, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xb8, 0x01, 0x0a, 0x16, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x50, 0x65, 0x65, 0x72, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x30, 0x0a, 0x14, 0x77, 0x69, + 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, + 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, + 0x61, 0x72, 0x64, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x18, 0x0a, 0x07, + 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, + 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x22, 0x6f, 0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x50, 0x72, 0x6f, 0x78, 0x79, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x28, 0x0a, 0x0d, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x48, 0x00, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x88, 0x01, 0x01, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, + 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x65, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x4f, 0x49, + 0x44, 0x43, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1d, 0x0a, 0x0a, + 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x72, + 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0b, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x72, 0x6c, 0x22, 0x26, + 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4f, 0x49, 0x44, 0x43, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x22, 0x55, 0x0a, 0x16, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, + 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xdc, 0x01, + 0x0a, 0x17, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x12, + 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x73, 0x65, 0x72, + 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x73, + 0x65, 0x72, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x64, 0x65, 0x6e, 0x69, 0x65, + 0x64, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, + 0x64, 0x65, 0x6e, 0x69, 0x65, 0x64, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0e, + 0x70, 0x65, 0x65, 0x72, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x05, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x65, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, + 0x64, 0x73, 0x12, 0x28, 0x0a, 0x10, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x70, 0x65, + 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x22, 0x50, 0x0a, 0x19, + 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x50, 0x65, + 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x75, 0x6e, + 0x6e, 0x65, 0x6c, 0x5f, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x75, + 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x22, 0x84, + 0x02, 0x0a, 0x1a, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, + 0x6c, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, @@ -3518,208 +3802,230 @@ var file_proxy_service_proto_rawDesc = []byte{ 0x52, 0x09, 0x75, 0x73, 0x65, 0x72, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x64, 0x65, 0x6e, 0x69, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x64, 0x65, 0x6e, 0x69, 0x65, 0x64, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, - 0x12, 0x24, 0x0a, 0x0e, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, - 0x64, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x65, 0x65, 0x72, 0x47, 0x72, - 0x6f, 0x75, 0x70, 0x49, 0x64, 0x73, 0x12, 0x28, 0x0a, 0x10, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x67, - 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x0e, 0x70, 0x65, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x4e, 0x61, 0x6d, 0x65, 0x73, - 0x22, 0x50, 0x0a, 0x19, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x75, 0x6e, 0x6e, - 0x65, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, - 0x09, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, - 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x22, 0x84, 0x02, 0x0a, 0x1a, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, - 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, - 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, - 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x73, 0x65, 0x72, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x12, - 0x23, 0x0a, 0x0d, 0x64, 0x65, 0x6e, 0x69, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x64, 0x65, 0x6e, 0x69, 0x65, 0x64, 0x52, 0x65, - 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, - 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x65, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x24, 0x0a, 0x0e, 0x70, 0x65, 0x65, - 0x72, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, - 0x09, 0x52, 0x0c, 0x70, 0x65, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x73, 0x12, - 0x28, 0x0a, 0x10, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x6e, 0x61, - 0x6d, 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x70, 0x65, 0x65, 0x72, 0x47, - 0x72, 0x6f, 0x75, 0x70, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x22, 0x81, 0x01, 0x0a, 0x13, 0x53, 0x79, - 0x6e, 0x63, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x32, 0x0a, 0x04, 0x69, 0x6e, 0x69, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x79, 0x6e, - 0x63, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x49, 0x6e, 0x69, 0x74, 0x48, 0x00, 0x52, - 0x04, 0x69, 0x6e, 0x69, 0x74, 0x12, 0x2f, 0x0a, 0x03, 0x61, 0x63, 0x6b, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x41, 0x63, 0x6b, 0x48, - 0x00, 0x52, 0x03, 0x61, 0x63, 0x6b, 0x42, 0x05, 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x22, 0xdf, 0x01, - 0x0a, 0x10, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x49, 0x6e, - 0x69, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x49, 0x64, 0x12, 0x18, 0x0a, - 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x39, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, - 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, - 0x41, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x41, 0x0a, 0x0c, - 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x50, 0x72, 0x6f, 0x78, 0x79, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, - 0x73, 0x52, 0x0c, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x22, - 0x11, 0x0a, 0x0f, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x41, - 0x63, 0x6b, 0x22, 0x7e, 0x0a, 0x14, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, - 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x07, 0x6d, 0x61, - 0x70, 0x70, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x61, - 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x07, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x32, - 0x0a, 0x15, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x73, 0x79, 0x6e, 0x63, 0x5f, 0x63, - 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x69, - 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x79, 0x6e, 0x63, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, - 0x74, 0x65, 0x22, 0xa9, 0x01, 0x0a, 0x1b, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4c, 0x4c, 0x4d, 0x50, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, - 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x67, 0x72, - 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x67, - 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, - 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x22, 0xff, - 0x01, 0x0a, 0x1c, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4c, 0x4c, 0x4d, 0x50, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x1a, 0x0a, 0x08, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2c, 0x0a, 0x12, 0x73, - 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x69, - 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, - 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x64, 0x12, 0x30, 0x0a, 0x14, 0x61, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, - 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x77, - 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x0d, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x53, 0x65, 0x63, 0x6f, 0x6e, - 0x64, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x6e, 0x79, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x6e, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, - 0x1f, 0x0a, 0x0b, 0x64, 0x65, 0x6e, 0x79, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x65, 0x6e, 0x79, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, - 0x22, 0x91, 0x02, 0x0a, 0x15, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x4c, 0x4d, 0x55, 0x73, - 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, - 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, - 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, - 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x25, 0x0a, + 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, + 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x24, 0x0a, 0x0e, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x67, 0x72, + 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x70, + 0x65, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x73, 0x12, 0x28, 0x0a, 0x10, 0x70, + 0x65, 0x65, 0x72, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, + 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x70, 0x65, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, + 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x22, 0xdb, 0x01, 0x0a, 0x13, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x61, + 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, + 0x04, 0x69, 0x6e, 0x69, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x61, 0x70, + 0x70, 0x69, 0x6e, 0x67, 0x73, 0x49, 0x6e, 0x69, 0x74, 0x48, 0x00, 0x52, 0x04, 0x69, 0x6e, 0x69, + 0x74, 0x12, 0x2f, 0x0a, 0x03, 0x61, 0x63, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x79, 0x6e, 0x63, + 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x41, 0x63, 0x6b, 0x48, 0x00, 0x52, 0x03, 0x61, + 0x63, 0x6b, 0x12, 0x58, 0x0a, 0x16, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x5f, 0x64, 0x69, 0x73, 0x63, + 0x6f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x52, 0x65, + 0x73, 0x75, 0x6c, 0x74, 0x48, 0x00, 0x52, 0x14, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x44, 0x69, 0x73, + 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x42, 0x05, 0x0a, 0x03, + 0x6d, 0x73, 0x67, 0x22, 0xdf, 0x01, 0x0a, 0x10, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x61, 0x70, 0x70, + 0x69, 0x6e, 0x67, 0x73, 0x49, 0x6e, 0x69, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x78, + 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x78, + 0x79, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x39, 0x0a, + 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x73, + 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x12, 0x41, 0x0a, 0x0c, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, + 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x43, 0x61, 0x70, 0x61, 0x62, + 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x0c, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, + 0x69, 0x74, 0x69, 0x65, 0x73, 0x22, 0x11, 0x0a, 0x0f, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x61, 0x70, + 0x70, 0x69, 0x6e, 0x67, 0x73, 0x41, 0x63, 0x6b, 0x22, 0xd9, 0x01, 0x0a, 0x14, 0x53, 0x79, 0x6e, + 0x63, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x32, 0x0a, 0x07, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x07, 0x6d, 0x61, + 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x32, 0x0a, 0x15, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, + 0x5f, 0x73, 0x79, 0x6e, 0x63, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x79, 0x6e, + 0x63, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x59, 0x0a, 0x17, 0x6d, 0x6f, 0x64, + 0x65, 0x6c, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x72, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x44, 0x69, 0x73, + 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x15, 0x6d, + 0x6f, 0x64, 0x65, 0x6c, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x22, 0xa9, 0x01, 0x0a, 0x1b, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4c, 0x4c, + 0x4d, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, + 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, + 0x64, 0x65, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, + 0x22, 0xff, 0x01, 0x0a, 0x1c, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4c, 0x4c, 0x4d, 0x50, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2c, 0x0a, + 0x12, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x73, 0x65, 0x6c, 0x65, 0x63, + 0x74, 0x65, 0x64, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x64, 0x12, 0x30, 0x0a, 0x14, 0x61, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, + 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x53, 0x65, 0x63, - 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x5f, 0x69, - 0x6e, 0x70, 0x75, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x74, 0x6f, 0x6b, 0x65, - 0x6e, 0x73, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x74, 0x6f, 0x6b, 0x65, 0x6e, - 0x73, 0x5f, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, - 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x19, 0x0a, 0x08, - 0x63, 0x6f, 0x73, 0x74, 0x5f, 0x75, 0x73, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x01, 0x52, 0x07, - 0x63, 0x6f, 0x73, 0x74, 0x55, 0x73, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x67, 0x72, 0x6f, 0x75, 0x70, - 0x5f, 0x69, 0x64, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x67, 0x72, 0x6f, 0x75, - 0x70, 0x49, 0x64, 0x73, 0x22, 0x18, 0x0a, 0x16, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x4c, - 0x4d, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2a, 0x64, - 0x0a, 0x16, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x17, 0x0a, 0x13, 0x55, 0x50, 0x44, 0x41, - 0x54, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, - 0x00, 0x12, 0x18, 0x0a, 0x14, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, - 0x5f, 0x4d, 0x4f, 0x44, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, 0x55, - 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52, 0x45, 0x4d, 0x4f, 0x56, - 0x45, 0x44, 0x10, 0x02, 0x2a, 0x46, 0x0a, 0x0f, 0x50, 0x61, 0x74, 0x68, 0x52, 0x65, 0x77, 0x72, - 0x69, 0x74, 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x14, 0x50, 0x41, 0x54, 0x48, 0x5f, - 0x52, 0x45, 0x57, 0x52, 0x49, 0x54, 0x45, 0x5f, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, - 0x00, 0x12, 0x19, 0x0a, 0x15, 0x50, 0x41, 0x54, 0x48, 0x5f, 0x52, 0x45, 0x57, 0x52, 0x49, 0x54, - 0x45, 0x5f, 0x50, 0x52, 0x45, 0x53, 0x45, 0x52, 0x56, 0x45, 0x10, 0x01, 0x2a, 0x90, 0x01, 0x0a, - 0x0e, 0x4d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, 0x72, 0x65, 0x53, 0x6c, 0x6f, 0x74, 0x12, - 0x1f, 0x0a, 0x1b, 0x4d, 0x49, 0x44, 0x44, 0x4c, 0x45, 0x57, 0x41, 0x52, 0x45, 0x5f, 0x53, 0x4c, - 0x4f, 0x54, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, - 0x12, 0x1e, 0x0a, 0x1a, 0x4d, 0x49, 0x44, 0x44, 0x4c, 0x45, 0x57, 0x41, 0x52, 0x45, 0x5f, 0x53, - 0x4c, 0x4f, 0x54, 0x5f, 0x4f, 0x4e, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x45, 0x53, 0x54, 0x10, 0x01, + 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x6e, 0x79, 0x5f, 0x63, 0x6f, 0x64, + 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x6e, 0x79, 0x43, 0x6f, 0x64, + 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x64, 0x65, 0x6e, 0x79, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x65, 0x6e, 0x79, 0x52, 0x65, 0x61, 0x73, + 0x6f, 0x6e, 0x22, 0x91, 0x02, 0x0a, 0x15, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x4c, 0x4d, + 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, + 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, + 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, + 0x65, 0x72, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, + 0x25, 0x0a, 0x0e, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, + 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x53, + 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, + 0x5f, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x73, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x73, 0x5f, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0c, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x19, + 0x0a, 0x08, 0x63, 0x6f, 0x73, 0x74, 0x5f, 0x75, 0x73, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x01, + 0x52, 0x07, 0x63, 0x6f, 0x73, 0x74, 0x55, 0x73, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x67, 0x72, 0x6f, + 0x75, 0x70, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x67, 0x72, + 0x6f, 0x75, 0x70, 0x49, 0x64, 0x73, 0x22, 0x18, 0x0a, 0x16, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, + 0x4c, 0x4c, 0x4d, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x80, 0x02, 0x0a, 0x15, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, + 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x75, 0x70, 0x73, + 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x75, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x55, 0x72, 0x6c, 0x12, 0x28, 0x0a, 0x10, + 0x61, 0x75, 0x74, 0x68, 0x5f, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x48, 0x65, 0x61, 0x64, + 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x68, + 0x65, 0x61, 0x64, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0f, 0x61, 0x75, 0x74, 0x68, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x73, 0x6b, 0x69, 0x70, 0x5f, 0x74, 0x6c, 0x73, 0x5f, 0x76, + 0x65, 0x72, 0x69, 0x66, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x73, 0x6b, 0x69, + 0x70, 0x54, 0x6c, 0x73, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x12, 0x27, 0x0a, 0x0f, 0x6f, 0x6c, + 0x6c, 0x61, 0x6d, 0x61, 0x5f, 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0e, 0x6f, 0x6c, 0x6c, 0x61, 0x6d, 0x61, 0x46, 0x61, 0x6c, 0x6c, 0x62, + 0x61, 0x63, 0x6b, 0x22, 0x3b, 0x0a, 0x13, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x44, 0x69, 0x73, 0x63, + 0x6f, 0x76, 0x65, 0x72, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, + 0x62, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, + 0x22, 0x9c, 0x01, 0x0a, 0x14, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, + 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x37, 0x0a, 0x06, 0x6d, 0x6f, 0x64, 0x65, + 0x6c, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x44, 0x69, 0x73, 0x63, 0x6f, + 0x76, 0x65, 0x72, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x52, 0x06, 0x6d, 0x6f, 0x64, 0x65, 0x6c, + 0x73, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x2a, + 0x64, 0x0a, 0x16, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x17, 0x0a, 0x13, 0x55, 0x50, 0x44, + 0x41, 0x54, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, + 0x10, 0x00, 0x12, 0x18, 0x0a, 0x14, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x59, 0x50, + 0x45, 0x5f, 0x4d, 0x4f, 0x44, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, + 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52, 0x45, 0x4d, 0x4f, + 0x56, 0x45, 0x44, 0x10, 0x02, 0x2a, 0x46, 0x0a, 0x0f, 0x50, 0x61, 0x74, 0x68, 0x52, 0x65, 0x77, + 0x72, 0x69, 0x74, 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x14, 0x50, 0x41, 0x54, 0x48, + 0x5f, 0x52, 0x45, 0x57, 0x52, 0x49, 0x54, 0x45, 0x5f, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, + 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x50, 0x41, 0x54, 0x48, 0x5f, 0x52, 0x45, 0x57, 0x52, 0x49, + 0x54, 0x45, 0x5f, 0x50, 0x52, 0x45, 0x53, 0x45, 0x52, 0x56, 0x45, 0x10, 0x01, 0x2a, 0x90, 0x01, + 0x0a, 0x0e, 0x4d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, 0x61, 0x72, 0x65, 0x53, 0x6c, 0x6f, 0x74, 0x12, 0x1f, 0x0a, 0x1b, 0x4d, 0x49, 0x44, 0x44, 0x4c, 0x45, 0x57, 0x41, 0x52, 0x45, 0x5f, 0x53, - 0x4c, 0x4f, 0x54, 0x5f, 0x4f, 0x4e, 0x5f, 0x52, 0x45, 0x53, 0x50, 0x4f, 0x4e, 0x53, 0x45, 0x10, - 0x02, 0x12, 0x1c, 0x0a, 0x18, 0x4d, 0x49, 0x44, 0x44, 0x4c, 0x45, 0x57, 0x41, 0x52, 0x45, 0x5f, - 0x53, 0x4c, 0x4f, 0x54, 0x5f, 0x54, 0x45, 0x52, 0x4d, 0x49, 0x4e, 0x41, 0x4c, 0x10, 0x03, 0x2a, - 0xc8, 0x01, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, - 0x18, 0x0a, 0x14, 0x50, 0x52, 0x4f, 0x58, 0x59, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, - 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x50, 0x52, 0x4f, - 0x58, 0x59, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56, 0x45, - 0x10, 0x01, 0x12, 0x23, 0x0a, 0x1f, 0x50, 0x52, 0x4f, 0x58, 0x59, 0x5f, 0x53, 0x54, 0x41, 0x54, - 0x55, 0x53, 0x5f, 0x54, 0x55, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x43, 0x52, - 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x24, 0x0a, 0x20, 0x50, 0x52, 0x4f, 0x58, 0x59, - 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, - 0x41, 0x54, 0x45, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x03, 0x12, 0x23, 0x0a, - 0x1f, 0x50, 0x52, 0x4f, 0x58, 0x59, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x43, 0x45, - 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, - 0x10, 0x04, 0x12, 0x16, 0x0a, 0x12, 0x50, 0x52, 0x4f, 0x58, 0x59, 0x5f, 0x53, 0x54, 0x41, 0x54, - 0x55, 0x53, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x05, 0x32, 0xfc, 0x07, 0x0a, 0x0c, 0x50, - 0x72, 0x6f, 0x78, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x5f, 0x0a, 0x10, 0x47, - 0x65, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, - 0x23, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x47, 0x65, 0x74, - 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x55, 0x0a, 0x0c, - 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x1f, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x61, - 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x4d, - 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, - 0x01, 0x30, 0x01, 0x12, 0x54, 0x0a, 0x0d, 0x53, 0x65, 0x6e, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, - 0x73, 0x4c, 0x6f, 0x67, 0x12, 0x20, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x67, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x6f, - 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x51, 0x0a, 0x0c, 0x41, 0x75, 0x74, - 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, - 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5d, 0x0a, 0x10, - 0x53, 0x65, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x12, 0x23, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, - 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x4c, 0x4f, 0x54, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, + 0x00, 0x12, 0x1e, 0x0a, 0x1a, 0x4d, 0x49, 0x44, 0x44, 0x4c, 0x45, 0x57, 0x41, 0x52, 0x45, 0x5f, + 0x53, 0x4c, 0x4f, 0x54, 0x5f, 0x4f, 0x4e, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x45, 0x53, 0x54, 0x10, + 0x01, 0x12, 0x1f, 0x0a, 0x1b, 0x4d, 0x49, 0x44, 0x44, 0x4c, 0x45, 0x57, 0x41, 0x52, 0x45, 0x5f, + 0x53, 0x4c, 0x4f, 0x54, 0x5f, 0x4f, 0x4e, 0x5f, 0x52, 0x45, 0x53, 0x50, 0x4f, 0x4e, 0x53, 0x45, + 0x10, 0x02, 0x12, 0x1c, 0x0a, 0x18, 0x4d, 0x49, 0x44, 0x44, 0x4c, 0x45, 0x57, 0x41, 0x52, 0x45, + 0x5f, 0x53, 0x4c, 0x4f, 0x54, 0x5f, 0x54, 0x45, 0x52, 0x4d, 0x49, 0x4e, 0x41, 0x4c, 0x10, 0x03, + 0x2a, 0xc8, 0x01, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x12, 0x18, 0x0a, 0x14, 0x50, 0x52, 0x4f, 0x58, 0x59, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, + 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x50, 0x52, + 0x4f, 0x58, 0x59, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56, + 0x45, 0x10, 0x01, 0x12, 0x23, 0x0a, 0x1f, 0x50, 0x52, 0x4f, 0x58, 0x59, 0x5f, 0x53, 0x54, 0x41, + 0x54, 0x55, 0x53, 0x5f, 0x54, 0x55, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x43, + 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x24, 0x0a, 0x20, 0x50, 0x52, 0x4f, 0x58, + 0x59, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, + 0x43, 0x41, 0x54, 0x45, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x03, 0x12, 0x23, + 0x0a, 0x1f, 0x50, 0x52, 0x4f, 0x58, 0x59, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x43, + 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, + 0x44, 0x10, 0x04, 0x12, 0x16, 0x0a, 0x12, 0x50, 0x52, 0x4f, 0x58, 0x59, 0x5f, 0x53, 0x54, 0x41, + 0x54, 0x55, 0x53, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x05, 0x32, 0xfc, 0x07, 0x0a, 0x0c, + 0x50, 0x72, 0x6f, 0x78, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x5f, 0x0a, 0x10, + 0x47, 0x65, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x12, 0x23, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x47, 0x65, + 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a, 0x0f, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x50, 0x65, 0x65, 0x72, 0x12, 0x22, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x50, 0x65, 0x65, 0x72, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x4f, 0x49, - 0x44, 0x43, 0x55, 0x52, 0x4c, 0x12, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x49, 0x44, 0x43, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x49, 0x44, 0x43, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a, 0x0f, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, - 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, - 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x63, 0x0a, 0x12, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x75, 0x6e, 0x6e, - 0x65, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x12, 0x25, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x75, 0x6e, 0x6e, - 0x65, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, - 0x61, 0x74, 0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x69, 0x0a, 0x14, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4c, 0x4c, - 0x4d, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x12, 0x27, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, - 0x4c, 0x4c, 0x4d, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4c, 0x4c, 0x4d, 0x50, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x57, 0x0a, 0x0e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x4c, 0x4d, 0x55, 0x73, 0x61, - 0x67, 0x65, 0x12, 0x21, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x4c, 0x4d, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x4c, 0x4d, 0x55, 0x73, 0x61, 0x67, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6e, 0x74, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x55, 0x0a, + 0x0c, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x1f, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x79, 0x6e, 0x63, 0x4d, + 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x79, 0x6e, 0x63, + 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x28, 0x01, 0x30, 0x01, 0x12, 0x54, 0x0a, 0x0d, 0x53, 0x65, 0x6e, 0x64, 0x41, 0x63, 0x63, 0x65, + 0x73, 0x73, 0x4c, 0x6f, 0x67, 0x12, 0x20, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x67, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, + 0x6f, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x51, 0x0a, 0x0c, 0x41, 0x75, + 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5d, 0x0a, + 0x10, 0x53, 0x65, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x12, 0x23, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, + 0x65, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a, 0x0f, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x50, 0x65, 0x65, 0x72, 0x12, + 0x22, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x50, 0x65, 0x65, 0x72, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x4f, + 0x49, 0x44, 0x43, 0x55, 0x52, 0x4c, 0x12, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x49, 0x44, 0x43, 0x55, 0x52, 0x4c, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x49, 0x44, 0x43, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a, 0x0f, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, + 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, + 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x63, 0x0a, 0x12, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x75, 0x6e, + 0x6e, 0x65, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x12, 0x25, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x75, 0x6e, + 0x6e, 0x65, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x56, 0x61, 0x6c, 0x69, + 0x64, 0x61, 0x74, 0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x69, 0x0a, 0x14, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4c, + 0x4c, 0x4d, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x12, 0x27, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x68, 0x65, 0x63, + 0x6b, 0x4c, 0x4c, 0x4d, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4c, 0x4c, 0x4d, 0x50, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x57, 0x0a, 0x0e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x4c, 0x4d, 0x55, 0x73, + 0x61, 0x67, 0x65, 0x12, 0x21, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x4c, 0x4d, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x4c, 0x4d, 0x55, 0x73, 0x61, + 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -3735,7 +4041,7 @@ func file_proxy_service_proto_rawDescGZIP() []byte { } var file_proxy_service_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_proxy_service_proto_msgTypes = make([]protoimpl.MessageInfo, 39) +var file_proxy_service_proto_msgTypes = make([]protoimpl.MessageInfo, 42) var file_proxy_service_proto_goTypes = []interface{}{ (ProxyMappingUpdateType)(0), // 0: management.ProxyMappingUpdateType (PathRewriteMode)(0), // 1: management.PathRewriteMode @@ -3779,23 +4085,26 @@ var file_proxy_service_proto_goTypes = []interface{}{ (*CheckLLMPolicyLimitsResponse)(nil), // 39: management.CheckLLMPolicyLimitsResponse (*RecordLLMUsageRequest)(nil), // 40: management.RecordLLMUsageRequest (*RecordLLMUsageResponse)(nil), // 41: management.RecordLLMUsageResponse - nil, // 42: management.PathTargetOptions.CustomHeadersEntry - nil, // 43: management.AccessLog.MetadataEntry - (*timestamppb.Timestamp)(nil), // 44: google.protobuf.Timestamp - (*durationpb.Duration)(nil), // 45: google.protobuf.Duration + (*ModelDiscoveryRequest)(nil), // 42: management.ModelDiscoveryRequest + (*ModelDiscoveryModel)(nil), // 43: management.ModelDiscoveryModel + (*ModelDiscoveryResult)(nil), // 44: management.ModelDiscoveryResult + nil, // 45: management.PathTargetOptions.CustomHeadersEntry + nil, // 46: management.AccessLog.MetadataEntry + (*timestamppb.Timestamp)(nil), // 47: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 48: google.protobuf.Duration } var file_proxy_service_proto_depIdxs = []int32{ - 44, // 0: management.GetMappingUpdateRequest.started_at:type_name -> google.protobuf.Timestamp + 47, // 0: management.GetMappingUpdateRequest.started_at:type_name -> google.protobuf.Timestamp 5, // 1: management.GetMappingUpdateRequest.capabilities:type_name -> management.ProxyCapabilities 14, // 2: management.GetMappingUpdateResponse.mapping:type_name -> management.ProxyMapping - 45, // 3: management.PathTargetOptions.request_timeout:type_name -> google.protobuf.Duration + 48, // 3: management.PathTargetOptions.request_timeout:type_name -> google.protobuf.Duration 1, // 4: management.PathTargetOptions.path_rewrite:type_name -> management.PathRewriteMode - 42, // 5: management.PathTargetOptions.custom_headers:type_name -> management.PathTargetOptions.CustomHeadersEntry - 45, // 6: management.PathTargetOptions.session_idle_timeout:type_name -> google.protobuf.Duration + 45, // 5: management.PathTargetOptions.custom_headers:type_name -> management.PathTargetOptions.CustomHeadersEntry + 48, // 6: management.PathTargetOptions.session_idle_timeout:type_name -> google.protobuf.Duration 9, // 7: management.PathTargetOptions.middlewares:type_name -> management.MiddlewareConfig 2, // 8: management.MiddlewareConfig.slot:type_name -> management.MiddlewareSlot 4, // 9: management.MiddlewareConfig.fail_mode:type_name -> management.MiddlewareConfig.FailMode - 45, // 10: management.MiddlewareConfig.timeout:type_name -> google.protobuf.Duration + 48, // 10: management.MiddlewareConfig.timeout:type_name -> google.protobuf.Duration 8, // 11: management.PathMapping.options:type_name -> management.PathTargetOptions 11, // 12: management.Authentication.header_auths:type_name -> management.HeaderAuth 0, // 13: management.ProxyMapping.type:type_name -> management.ProxyMappingUpdateType @@ -3803,8 +4112,8 @@ var file_proxy_service_proto_depIdxs = []int32{ 12, // 15: management.ProxyMapping.auth:type_name -> management.Authentication 13, // 16: management.ProxyMapping.access_restrictions:type_name -> management.AccessRestrictions 17, // 17: management.SendAccessLogRequest.log:type_name -> management.AccessLog - 44, // 18: management.AccessLog.timestamp:type_name -> google.protobuf.Timestamp - 43, // 19: management.AccessLog.metadata:type_name -> management.AccessLog.MetadataEntry + 47, // 18: management.AccessLog.timestamp:type_name -> google.protobuf.Timestamp + 46, // 19: management.AccessLog.metadata:type_name -> management.AccessLog.MetadataEntry 20, // 20: management.AuthenticateRequest.password:type_name -> management.PasswordRequest 21, // 21: management.AuthenticateRequest.pin:type_name -> management.PinRequest 19, // 22: management.AuthenticateRequest.header_auth:type_name -> management.HeaderAuthRequest @@ -3812,36 +4121,39 @@ var file_proxy_service_proto_depIdxs = []int32{ 24, // 24: management.SendStatusUpdateRequest.inbound_listener:type_name -> management.ProxyInboundListener 35, // 25: management.SyncMappingsRequest.init:type_name -> management.SyncMappingsInit 36, // 26: management.SyncMappingsRequest.ack:type_name -> management.SyncMappingsAck - 44, // 27: management.SyncMappingsInit.started_at:type_name -> google.protobuf.Timestamp - 5, // 28: management.SyncMappingsInit.capabilities:type_name -> management.ProxyCapabilities - 14, // 29: management.SyncMappingsResponse.mapping:type_name -> management.ProxyMapping - 6, // 30: management.ProxyService.GetMappingUpdate:input_type -> management.GetMappingUpdateRequest - 34, // 31: management.ProxyService.SyncMappings:input_type -> management.SyncMappingsRequest - 15, // 32: management.ProxyService.SendAccessLog:input_type -> management.SendAccessLogRequest - 18, // 33: management.ProxyService.Authenticate:input_type -> management.AuthenticateRequest - 23, // 34: management.ProxyService.SendStatusUpdate:input_type -> management.SendStatusUpdateRequest - 26, // 35: management.ProxyService.CreateProxyPeer:input_type -> management.CreateProxyPeerRequest - 28, // 36: management.ProxyService.GetOIDCURL:input_type -> management.GetOIDCURLRequest - 30, // 37: management.ProxyService.ValidateSession:input_type -> management.ValidateSessionRequest - 32, // 38: management.ProxyService.ValidateTunnelPeer:input_type -> management.ValidateTunnelPeerRequest - 38, // 39: management.ProxyService.CheckLLMPolicyLimits:input_type -> management.CheckLLMPolicyLimitsRequest - 40, // 40: management.ProxyService.RecordLLMUsage:input_type -> management.RecordLLMUsageRequest - 7, // 41: management.ProxyService.GetMappingUpdate:output_type -> management.GetMappingUpdateResponse - 37, // 42: management.ProxyService.SyncMappings:output_type -> management.SyncMappingsResponse - 16, // 43: management.ProxyService.SendAccessLog:output_type -> management.SendAccessLogResponse - 22, // 44: management.ProxyService.Authenticate:output_type -> management.AuthenticateResponse - 25, // 45: management.ProxyService.SendStatusUpdate:output_type -> management.SendStatusUpdateResponse - 27, // 46: management.ProxyService.CreateProxyPeer:output_type -> management.CreateProxyPeerResponse - 29, // 47: management.ProxyService.GetOIDCURL:output_type -> management.GetOIDCURLResponse - 31, // 48: management.ProxyService.ValidateSession:output_type -> management.ValidateSessionResponse - 33, // 49: management.ProxyService.ValidateTunnelPeer:output_type -> management.ValidateTunnelPeerResponse - 39, // 50: management.ProxyService.CheckLLMPolicyLimits:output_type -> management.CheckLLMPolicyLimitsResponse - 41, // 51: management.ProxyService.RecordLLMUsage:output_type -> management.RecordLLMUsageResponse - 41, // [41:52] is the sub-list for method output_type - 30, // [30:41] is the sub-list for method input_type - 30, // [30:30] is the sub-list for extension type_name - 30, // [30:30] is the sub-list for extension extendee - 0, // [0:30] is the sub-list for field type_name + 44, // 27: management.SyncMappingsRequest.model_discovery_result:type_name -> management.ModelDiscoveryResult + 47, // 28: management.SyncMappingsInit.started_at:type_name -> google.protobuf.Timestamp + 5, // 29: management.SyncMappingsInit.capabilities:type_name -> management.ProxyCapabilities + 14, // 30: management.SyncMappingsResponse.mapping:type_name -> management.ProxyMapping + 42, // 31: management.SyncMappingsResponse.model_discovery_request:type_name -> management.ModelDiscoveryRequest + 43, // 32: management.ModelDiscoveryResult.models:type_name -> management.ModelDiscoveryModel + 6, // 33: management.ProxyService.GetMappingUpdate:input_type -> management.GetMappingUpdateRequest + 34, // 34: management.ProxyService.SyncMappings:input_type -> management.SyncMappingsRequest + 15, // 35: management.ProxyService.SendAccessLog:input_type -> management.SendAccessLogRequest + 18, // 36: management.ProxyService.Authenticate:input_type -> management.AuthenticateRequest + 23, // 37: management.ProxyService.SendStatusUpdate:input_type -> management.SendStatusUpdateRequest + 26, // 38: management.ProxyService.CreateProxyPeer:input_type -> management.CreateProxyPeerRequest + 28, // 39: management.ProxyService.GetOIDCURL:input_type -> management.GetOIDCURLRequest + 30, // 40: management.ProxyService.ValidateSession:input_type -> management.ValidateSessionRequest + 32, // 41: management.ProxyService.ValidateTunnelPeer:input_type -> management.ValidateTunnelPeerRequest + 38, // 42: management.ProxyService.CheckLLMPolicyLimits:input_type -> management.CheckLLMPolicyLimitsRequest + 40, // 43: management.ProxyService.RecordLLMUsage:input_type -> management.RecordLLMUsageRequest + 7, // 44: management.ProxyService.GetMappingUpdate:output_type -> management.GetMappingUpdateResponse + 37, // 45: management.ProxyService.SyncMappings:output_type -> management.SyncMappingsResponse + 16, // 46: management.ProxyService.SendAccessLog:output_type -> management.SendAccessLogResponse + 22, // 47: management.ProxyService.Authenticate:output_type -> management.AuthenticateResponse + 25, // 48: management.ProxyService.SendStatusUpdate:output_type -> management.SendStatusUpdateResponse + 27, // 49: management.ProxyService.CreateProxyPeer:output_type -> management.CreateProxyPeerResponse + 29, // 50: management.ProxyService.GetOIDCURL:output_type -> management.GetOIDCURLResponse + 31, // 51: management.ProxyService.ValidateSession:output_type -> management.ValidateSessionResponse + 33, // 52: management.ProxyService.ValidateTunnelPeer:output_type -> management.ValidateTunnelPeerResponse + 39, // 53: management.ProxyService.CheckLLMPolicyLimits:output_type -> management.CheckLLMPolicyLimitsResponse + 41, // 54: management.ProxyService.RecordLLMUsage:output_type -> management.RecordLLMUsageResponse + 44, // [44:55] is the sub-list for method output_type + 33, // [33:44] is the sub-list for method input_type + 33, // [33:33] is the sub-list for extension type_name + 33, // [33:33] is the sub-list for extension extendee + 0, // [0:33] is the sub-list for field type_name } func init() { file_proxy_service_proto_init() } @@ -4294,6 +4606,42 @@ func file_proxy_service_proto_init() { return nil } } + file_proxy_service_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ModelDiscoveryRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proxy_service_proto_msgTypes[38].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ModelDiscoveryModel); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proxy_service_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ModelDiscoveryResult); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_proxy_service_proto_msgTypes[0].OneofWrappers = []interface{}{} file_proxy_service_proto_msgTypes[13].OneofWrappers = []interface{}{ @@ -4306,6 +4654,7 @@ func file_proxy_service_proto_init() { file_proxy_service_proto_msgTypes[29].OneofWrappers = []interface{}{ (*SyncMappingsRequest_Init)(nil), (*SyncMappingsRequest_Ack)(nil), + (*SyncMappingsRequest_ModelDiscoveryResult)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -4313,7 +4662,7 @@ func file_proxy_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_proxy_service_proto_rawDesc, NumEnums: 5, - NumMessages: 39, + NumMessages: 42, NumExtensions: 0, NumServices: 1, }, diff --git a/shared/management/proto/proxy_service.proto b/shared/management/proto/proxy_service.proto index 89d1f8749..03142043c 100644 --- a/shared/management/proto/proxy_service.proto +++ b/shared/management/proto/proxy_service.proto @@ -73,6 +73,10 @@ message ProxyCapabilities { optional bool private = 4; // Whether the proxy enforces ProxyMapping.private (fails closed on ValidateTunnelPeer failure). Management MUST NOT stream private mappings to proxies that don't claim this. optional bool supports_private_service = 5; + // Whether the proxy can execute correlated Agent Network model-discovery + // requests delivered over SyncMappings. Management must not send discovery + // requests to proxies that omit or disable this capability. + optional bool supports_model_discovery = 6; } // GetMappingUpdateRequest is sent to initialise a mapping stream. @@ -420,6 +424,8 @@ message SyncMappingsRequest { oneof msg { SyncMappingsInit init = 1; SyncMappingsAck ack = 2; + // Correlated response to a ModelDiscoveryRequest received from management. + ModelDiscoveryResult model_discovery_result = 3; } } @@ -443,6 +449,9 @@ message SyncMappingsResponse { repeated ProxyMapping mapping = 1; // initial_sync_complete is set on the last message of the initial snapshot. bool initial_sync_complete = 2; + // An out-of-band model-discovery operation. This is sent only after the + // initial snapshot and only to proxies advertising supports_model_discovery. + ModelDiscoveryRequest model_discovery_request = 3; } // CheckLLMPolicyLimitsRequest carries the resolved caller identity and the @@ -501,3 +510,33 @@ message RecordLLMUsageRequest { message RecordLLMUsageResponse { } +// ModelDiscoveryRequest asks a proxy to list models from a persisted provider +// endpoint using the proxy host's network path. request_id is assigned by +// management and echoed verbatim in ModelDiscoveryResult. +message ModelDiscoveryRequest { + string request_id = 1; + string upstream_url = 2; + string auth_header_name = 3; + string auth_header_value = 4; + bool skip_tls_verify = 5; + // When true, the proxy may fall back from the OpenAI-compatible + // GET /v1/models endpoint to Ollama's native GET /api/tags endpoint. + bool ollama_fallback = 6; +} + +// ModelDiscoveryModel is the normalized model shape returned to management. +// Arbitrary upstream fields never cross the proxy control channel. +message ModelDiscoveryModel { + string id = 1; + string label = 2; +} + +// ModelDiscoveryResult completes one ModelDiscoveryRequest. error is empty on +// success and contains only a sanitized diagnostic on failure. +message ModelDiscoveryResult { + string request_id = 1; + repeated ModelDiscoveryModel models = 2; + // Stable source label, currently openai_v1_models or ollama_api_tags. + string source = 3; + string error = 4; +}