diff --git a/aibridge/recorder/drpc_recorder.go b/aibridge/recorder/drpc_recorder.go index f58667a9d24..daa10b3a1e8 100644 --- a/aibridge/recorder/drpc_recorder.go +++ b/aibridge/recorder/drpc_recorder.go @@ -10,6 +10,7 @@ import ( "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" + agplaibridge "github.com/coder/coder/v2/coderd/aibridge" "github.com/coder/coder/v2/coderd/aibridged/proto" ) @@ -22,7 +23,7 @@ type DRPCRecorder struct { } func (t *DRPCRecorder) RecordInterception(ctx context.Context, req *InterceptionRecord) error { - _, err := t.client.RecordInterception(ctx, &proto.RecordInterceptionRequest{ + in := &proto.RecordInterceptionRequest{ Id: req.ID, ApiKeyId: t.apiKeyID, InitiatorId: req.InitiatorID, @@ -39,7 +40,11 @@ func (t *DRPCRecorder) RecordInterception(ctx context.Context, req *Interception CredentialHint: req.CredentialHint, AgentFirewallSessionId: req.AgentFirewallSessionID, AgentFirewallSequenceNumber: req.AgentFirewallSequenceNumber, - }) + } + if attr, ok := agplaibridge.AttributionFromContext(ctx); ok { + in.WorkspaceId = attr.WorkspaceID.String() + } + _, err := t.client.RecordInterception(ctx, in) return err } diff --git a/coderd/aibridge/factory.go b/coderd/aibridge/factory.go index 6b2e7b9a63d..4a51f42c33c 100644 --- a/coderd/aibridge/factory.go +++ b/coderd/aibridge/factory.go @@ -3,8 +3,33 @@ package aibridge import ( "context" "net/http" + + "github.com/google/uuid" ) +// Attribution carries contextual per-request attribution data for a request. +type Attribution struct { + // WorkspaceID is the workspace bound to the chat, or uuid.Nil when no + // workspace is bound. + WorkspaceID uuid.UUID +} + +type attributionCtxKey struct{} + +// WithAttribution returns a copy of ctx carrying the trusted Attribution. +// Attribution MUST ONLY set by authentication or delegated in-process code, +// and NEVER by client-provided HTTP headers. +func WithAttribution(ctx context.Context, attr Attribution) context.Context { + return context.WithValue(ctx, attributionCtxKey{}, attr) +} + +// AttributionFromContext returns the Attribution attached by [WithAttribution] +// and whether a non-zero WorkspaceID was present. +func AttributionFromContext(ctx context.Context) (Attribution, bool) { + attr, ok := ctx.Value(attributionCtxKey{}).(Attribution) + return attr, ok && attr.WorkspaceID != uuid.Nil +} + // Source identifies the call site that asked aibridge for a transport. It is // attached to the request context so downstream handlers and logs can attribute // traffic without changing behavior based on the value. @@ -29,7 +54,10 @@ func SourceFromContext(ctx context.Context) Source { return src } -type delegatedAPIKeyIDCtxKey struct{} +type ( + deletedAPIKeyIDCtxKey struct{} + delegatedAttributionCtxKey struct{} +) // WithDelegatedAPIKeyID returns a copy of ctx carrying an API key ID on whose // behalf the request is being made. The in-process aibridge transport requires @@ -40,16 +68,31 @@ type delegatedAPIKeyIDCtxKey struct{} // has not expired, and belongs to a non-deleted, non-system user. It does not // verify the key secret, because the caller never has it. func WithDelegatedAPIKeyID(ctx context.Context, id string) context.Context { - return context.WithValue(ctx, delegatedAPIKeyIDCtxKey{}, id) + /* + return WithDelegatedRequest(ctx, id, Attribution{}) + */ + return context.WithValue(ctx, deletedAPIKeyIDCtxKey{}, id) +} + +func WithDelegatedAttribution(ctx context.Context, attr Attribution) context.Context { + return context.WithValue(ctx, delegatedAttributionCtxKey{}, attr) } // DelegatedAPIKeyIDFromContext returns the API key ID attached by // [WithDelegatedAPIKeyID] and whether a non-empty value was set. func DelegatedAPIKeyIDFromContext(ctx context.Context) (string, bool) { - id, ok := ctx.Value(delegatedAPIKeyIDCtxKey{}).(string) + id, ok := ctx.Value(deletedAPIKeyIDCtxKey{}).(string) return id, ok && id != "" } +// DelegatedAttributionFromContext returns the trusted attribution attached to +// the delegated request. Its boolean reports whether delegated authentication, +// rather than attribution itself, was present. +func DelegatedAttributionFromContext(ctx context.Context) (Attribution, bool) { + attr, ok := ctx.Value(delegatedAttributionCtxKey{}).(Attribution) + return attr, ok +} + // TransportFactory returns an [http.RoundTripper] that dispatches an aibridge // request in-process for a given provider instance name. // diff --git a/coderd/aibridge/factory_test.go b/coderd/aibridge/factory_test.go new file mode 100644 index 00000000000..3f9daeb5d91 --- /dev/null +++ b/coderd/aibridge/factory_test.go @@ -0,0 +1,64 @@ +package aibridge_test + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/coderd/aibridge" +) + +func TestAttributionContext(t *testing.T) { + t.Parallel() + + wsID := uuid.New() + + t.Run("WithWorkspace", func(t *testing.T) { + t.Parallel() + + attr := aibridge.Attribution{WorkspaceID: wsID} + ctx := aibridge.WithAttribution(context.Background(), attr) + got, ok := aibridge.AttributionFromContext(ctx) + require.True(t, ok) + require.Equal(t, attr, got) + }) + + t.Run("NoAttribution", func(t *testing.T) { + t.Parallel() + + _, ok := aibridge.AttributionFromContext(context.Background()) + require.False(t, ok) + }) + + t.Run("ZeroWorkspaceID", func(t *testing.T) { + t.Parallel() + + // An Attribution with a zero WorkspaceID is not considered set. + attr := aibridge.Attribution{WorkspaceID: uuid.Nil} + ctx := aibridge.WithAttribution(context.Background(), attr) + _, ok := aibridge.AttributionFromContext(ctx) + require.False(t, ok, "Attribution with zero WorkspaceID must not be considered set") + }) + + t.Run("OverwrittenByInnerContext", func(t *testing.T) { + t.Parallel() + + // A child context can shadow the parent's Attribution without + // mutating it, ensuring per-request immutability. + parent := aibridge.Attribution{WorkspaceID: uuid.New()} + child := aibridge.Attribution{WorkspaceID: wsID} + + pCtx := aibridge.WithAttribution(context.Background(), parent) + cCtx := aibridge.WithAttribution(pCtx, child) + + gotParent, ok := aibridge.AttributionFromContext(pCtx) + require.True(t, ok) + require.Equal(t, parent, gotParent) + + gotChild, ok := aibridge.AttributionFromContext(cCtx) + require.True(t, ok) + require.Equal(t, child, gotChild) + }) +} diff --git a/coderd/aibridged/http.go b/coderd/aibridged/http.go index 50e74c19fe3..efb66700abe 100644 --- a/coderd/aibridged/http.go +++ b/coderd/aibridged/http.go @@ -145,6 +145,23 @@ func (s *Server) ServeHTTP(rw http.ResponseWriter, r *http.Request) { } logger = logger.With(slog.F("user_id", id)) + // Direct and delegated carriers are mutually exclusive and request-scoped. + // Delegated requests carry attribution stamped by the in-process caller + // (chatd); direct requests carry workspace attribution parsed from the + // server-minted token name by IsAuthorized. + var attribution agplaibridge.Attribution + if delegated { + attribution, _ = agplaibridge.DelegatedAttributionFromContext(ctx) + } else { + attribution, err = attributionFromAuthorization(resp) + if err != nil { + logger.Warn(ctx, "invalid authorization attribution", slog.Error(err)) + http.Error(rw, ErrUnauthorized.Error(), http.StatusForbidden) + return + } + } + ctx = agplaibridge.WithAttribution(ctx, attribution) + budgetResp, err := client.IsBudgetExceeded(ctx, &proto.IsBudgetExceededRequest{ UserId: id.String(), }) @@ -183,3 +200,17 @@ func (s *Server) ServeHTTP(rw http.ResponseWriter, r *http.Request) { handler.ServeHTTP(rw, r) } + +// attributionFromAuthorization extracts the workspace ID from the +// IsAuthorizedResponse. The proto carries only workspace_id for attribution; +// organization and workspace name are not returned. +func attributionFromAuthorization(resp *proto.IsAuthorizedResponse) (agplaibridge.Attribution, error) { + if resp.GetWorkspaceId() == "" { + return agplaibridge.Attribution{}, nil + } + workspaceID, err := uuid.Parse(resp.GetWorkspaceId()) + if err != nil { + return agplaibridge.Attribution{}, xerrors.Errorf("parse workspace ID: %w", err) + } + return agplaibridge.Attribution{WorkspaceID: workspaceID}, nil +} diff --git a/coderd/aibridged/proto/aibridged.pb.go b/coderd/aibridged/proto/aibridged.pb.go index c6024bdf13d..5aab5a4c98f 100644 --- a/coderd/aibridged/proto/aibridged.pb.go +++ b/coderd/aibridged/proto/aibridged.pb.go @@ -48,6 +48,7 @@ type RecordInterceptionRequest struct { // used to order network requests relative to Agent Firewall audit events. // Absent when the request did not pass through Agent Firewall. AgentFirewallSequenceNumber *int32 `protobuf:"varint,16,opt,name=agent_firewall_sequence_number,json=agentFirewallSequenceNumber,proto3,oneof" json:"agent_firewall_sequence_number,omitempty"` + WorkspaceId string `protobuf:"bytes,17,opt,name=workspace_id,json=workspaceId,proto3" json:"workspace_id,omitempty"` // UUID. Empty when workspace attribution is unknown. } func (x *RecordInterceptionRequest) Reset() { @@ -194,6 +195,13 @@ func (x *RecordInterceptionRequest) GetAgentFirewallSequenceNumber() int32 { return 0 } +func (x *RecordInterceptionRequest) GetWorkspaceId() string { + if x != nil { + return x.WorkspaceId + } + return "" +} + type RecordInterceptionResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1241,9 +1249,10 @@ type IsAuthorizedResponse struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - OwnerId string `protobuf:"bytes,1,opt,name=owner_id,json=ownerId,proto3" json:"owner_id,omitempty"` - ApiKeyId string `protobuf:"bytes,2,opt,name=api_key_id,json=apiKeyId,proto3" json:"api_key_id,omitempty"` - Username string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"` + OwnerId string `protobuf:"bytes,1,opt,name=owner_id,json=ownerId,proto3" json:"owner_id,omitempty"` + ApiKeyId string `protobuf:"bytes,2,opt,name=api_key_id,json=apiKeyId,proto3" json:"api_key_id,omitempty"` + Username string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"` + WorkspaceId string `protobuf:"bytes,4,opt,name=workspace_id,json=workspaceId,proto3" json:"workspace_id,omitempty"` // UUID. Empty when workspace attribution is unknown. } func (x *IsAuthorizedResponse) Reset() { @@ -1299,6 +1308,13 @@ func (x *IsAuthorizedResponse) GetUsername() string { return "" } +func (x *IsAuthorizedResponse) GetWorkspaceId() string { + if x != nil { + return x.WorkspaceId + } + return "" +} + type IsBudgetExceededRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1794,7 +1810,7 @@ var file_coderd_aibridged_proto_aibridged_proto_rawDesc = []byte{ 0x19, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61, 0x6e, 0x79, 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, 0x93, 0x07, 0x0a, 0x19, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb6, 0x07, 0x0a, 0x19, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x6e, 0x69, @@ -1839,348 +1855,353 @@ var file_coderd_aibridged_proto_aibridged_proto_rawDesc = []byte{ 0x6c, 0x6c, 0x5f, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x10, 0x20, 0x01, 0x28, 0x05, 0x48, 0x03, 0x52, 0x1b, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x53, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, - 0x65, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x88, 0x01, 0x01, 0x1a, 0x51, 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, 0x2a, 0x0a, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, - 0x6e, 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x1b, 0x0a, - 0x19, 0x5f, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x74, 0x6f, - 0x6f, 0x6c, 0x5f, 0x63, 0x61, 0x6c, 0x6c, 0x5f, 0x69, 0x64, 0x42, 0x14, 0x0a, 0x12, 0x5f, 0x63, - 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, - 0x42, 0x1c, 0x0a, 0x1a, 0x5f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x66, 0x69, 0x72, 0x65, 0x77, - 0x61, 0x6c, 0x6c, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x42, 0x21, - 0x0a, 0x1f, 0x5f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, - 0x6c, 0x5f, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, - 0x72, 0x22, 0x1c, 0x0a, 0x1a, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, - 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0xff, 0x01, 0x0a, 0x1e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, - 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, - 0x69, 0x64, 0x12, 0x35, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x02, - 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, 0x07, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x41, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x72, 0x65, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x68, 0x69, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x48, 0x69, - 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0a, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x74, 0x79, 0x70, 0x65, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x54, - 0x79, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12, 0x28, 0x0a, 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, - 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, - 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x88, 0x01, 0x01, - 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, - 0x10, 0x0a, 0x0e, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x22, 0x21, 0x0a, 0x1f, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, - 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xe9, 0x03, 0x0a, 0x17, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, - 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x73, 0x67, - 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, 0x67, 0x49, 0x64, - 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x54, 0x6f, 0x6b, - 0x65, 0x6e, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, - 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, - 0x75, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x48, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, - 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 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, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, - 0x18, 0x06, 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, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x35, 0x0a, - 0x17, 0x63, 0x61, 0x63, 0x68, 0x65, 0x5f, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x69, 0x6e, 0x70, 0x75, - 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x14, - 0x63, 0x61, 0x63, 0x68, 0x65, 0x52, 0x65, 0x61, 0x64, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x37, 0x0a, 0x18, 0x63, 0x61, 0x63, 0x68, 0x65, 0x5f, 0x77, 0x72, - 0x69, 0x74, 0x65, 0x5f, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, - 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x15, 0x63, 0x61, 0x63, 0x68, 0x65, 0x57, 0x72, 0x69, - 0x74, 0x65, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x1a, 0x51, 0x0a, + 0x65, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, 0x21, 0x0a, 0x0c, 0x77, 0x6f, + 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x1a, 0x51, 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, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, - 0x22, 0x1a, 0x0a, 0x18, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, - 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xcb, 0x02, 0x0a, - 0x18, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, - 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x73, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, 0x67, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x6f, - 0x6d, 0x70, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, - 0x74, 0x12, 0x49, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, - 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 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, 0x39, 0x0a, 0x0a, - 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x05, 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, 0x63, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x1a, 0x51, 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, 0x2a, 0x0a, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x1b, 0x0a, 0x19, 0x52, 0x65, - 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xa8, 0x04, 0x0a, 0x16, 0x52, 0x65, 0x63, 0x6f, - 0x72, 0x64, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6d, - 0x73, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, 0x67, - 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x09, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x55, 0x72, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, - 0x70, 0x75, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, - 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x2e, 0x0a, 0x10, - 0x69, 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x0f, 0x69, 0x6e, 0x76, 0x6f, 0x63, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x88, 0x01, 0x01, 0x12, 0x47, 0x0a, 0x08, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f, - 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, + 0x42, 0x1b, 0x0a, 0x19, 0x5f, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6e, 0x67, + 0x5f, 0x74, 0x6f, 0x6f, 0x6c, 0x5f, 0x63, 0x61, 0x6c, 0x6c, 0x5f, 0x69, 0x64, 0x42, 0x14, 0x0a, + 0x12, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x5f, 0x69, 0x64, 0x42, 0x1c, 0x0a, 0x1a, 0x5f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x66, 0x69, + 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, + 0x64, 0x42, 0x21, 0x0a, 0x1f, 0x5f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x66, 0x69, 0x72, 0x65, + 0x77, 0x61, 0x6c, 0x6c, 0x5f, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x5f, 0x6e, 0x75, + 0x6d, 0x62, 0x65, 0x72, 0x22, 0x1c, 0x0a, 0x1a, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, + 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0xff, 0x01, 0x0a, 0x1e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, + 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x35, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x61, + 0x74, 0x18, 0x02, 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, 0x07, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x41, 0x74, 0x12, 0x27, 0x0a, 0x0f, + 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x68, 0x69, 0x6e, 0x74, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, + 0x6c, 0x48, 0x69, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0a, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x09, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12, 0x28, 0x0a, 0x0d, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x48, 0x01, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x88, 0x01, 0x01, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x22, 0x21, 0x0a, 0x1f, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, + 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xe9, 0x03, 0x0a, 0x17, 0x52, 0x65, 0x63, 0x6f, + 0x72, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, + 0x6d, 0x73, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, + 0x67, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x69, 0x6e, 0x70, 0x75, 0x74, + 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, + 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6f, + 0x75, 0x74, 0x70, 0x75, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x48, 0x0a, 0x08, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 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, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, - 0x5f, 0x61, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x5f, 0x61, 0x74, 0x18, 0x06, 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, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, - 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x6f, 0x6f, 0x6c, 0x5f, 0x63, 0x61, 0x6c, 0x6c, 0x5f, 0x69, 0x64, - 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x6f, 0x6f, 0x6c, 0x43, 0x61, 0x6c, 0x6c, - 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x69, 0x74, 0x65, 0x6d, 0x49, 0x64, 0x1a, 0x51, 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, 0x2a, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x0d, - 0x0a, 0x0b, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x42, 0x13, 0x0a, - 0x11, 0x5f, 0x69, 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x22, 0x19, 0x0a, 0x17, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f, 0x6c, - 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xb8, 0x02, - 0x0a, 0x19, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x54, 0x68, 0x6f, - 0x75, 0x67, 0x68, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x4a, - 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x2e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4d, - 0x6f, 0x64, 0x65, 0x6c, 0x54, 0x68, 0x6f, 0x75, 0x67, 0x68, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 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, 0x39, 0x0a, 0x0a, 0x63, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x04, 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, 0x63, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x64, 0x41, 0x74, 0x1a, 0x51, 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, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x1c, 0x0a, 0x1a, 0x52, 0x65, 0x63, 0x6f, - 0x72, 0x64, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x54, 0x68, 0x6f, 0x75, 0x67, 0x68, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x35, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0xb2, 0x01, - 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, - 0x10, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x6d, 0x63, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, - 0x0e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x4d, 0x63, 0x70, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x51, 0x0a, 0x19, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x75, 0x74, 0x68, - 0x5f, 0x6d, 0x63, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4d, 0x43, 0x50, 0x53, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x16, 0x65, 0x78, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x63, 0x70, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x73, 0x22, 0x85, 0x01, 0x0a, 0x0f, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x28, 0x0a, 0x10, 0x74, 0x6f, 0x6f, 0x6c, - 0x5f, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x65, 0x67, 0x65, 0x78, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0e, 0x74, 0x6f, 0x6f, 0x6c, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x67, - 0x65, 0x78, 0x12, 0x26, 0x0a, 0x0f, 0x74, 0x6f, 0x6f, 0x6c, 0x5f, 0x64, 0x65, 0x6e, 0x79, 0x5f, - 0x72, 0x65, 0x67, 0x65, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x74, 0x6f, 0x6f, - 0x6c, 0x44, 0x65, 0x6e, 0x79, 0x52, 0x65, 0x67, 0x65, 0x78, 0x22, 0x72, 0x0a, 0x24, 0x47, 0x65, - 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x31, 0x0a, 0x15, 0x6d, - 0x63, 0x70, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x5f, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x6d, 0x63, 0x70, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x49, 0x64, 0x73, 0x22, 0xda, - 0x02, 0x0a, 0x25, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, - 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x0d, 0x61, 0x63, 0x63, 0x65, - 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x3e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, - 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x41, 0x63, - 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, - 0x0c, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x50, 0x0a, - 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x38, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, - 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, - 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x1a, - 0x3f, 0x0a, 0x11, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 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, - 0x1a, 0x39, 0x0a, 0x0b, 0x45, 0x72, 0x72, 0x6f, 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, 0x3e, 0x0a, 0x13, 0x49, - 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x22, 0x6b, 0x0a, 0x14, 0x49, - 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1c, - 0x0a, 0x0a, 0x61, 0x70, 0x69, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x61, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, - 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x32, 0x0a, 0x17, 0x49, 0x73, 0x42, 0x75, - 0x64, 0x67, 0x65, 0x74, 0x45, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x80, 0x01, 0x0a, - 0x18, 0x49, 0x73, 0x42, 0x75, 0x64, 0x67, 0x65, 0x74, 0x45, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, - 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x78, 0x63, - 0x65, 0x65, 0x64, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x65, 0x78, 0x63, - 0x65, 0x65, 0x64, 0x65, 0x64, 0x12, 0x31, 0x0a, 0x12, 0x73, 0x70, 0x65, 0x6e, 0x64, 0x5f, 0x6c, - 0x69, 0x6d, 0x69, 0x74, 0x5f, 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x03, 0x48, 0x00, 0x52, 0x10, 0x73, 0x70, 0x65, 0x6e, 0x64, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4d, - 0x69, 0x63, 0x72, 0x6f, 0x73, 0x88, 0x01, 0x01, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x73, 0x70, 0x65, - 0x6e, 0x64, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x5f, 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x22, - 0x17, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x49, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x41, - 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x09, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x49, - 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, - 0x65, 0x72, 0x73, 0x22, 0x19, 0x0a, 0x17, 0x57, 0x61, 0x74, 0x63, 0x68, 0x41, 0x49, 0x50, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x1a, - 0x0a, 0x18, 0x57, 0x61, 0x74, 0x63, 0x68, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, - 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xb5, 0x01, 0x0a, 0x0a, 0x41, - 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, - 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, - 0x65, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x62, - 0x61, 0x73, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, - 0x61, 0x73, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x05, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x12, 0x36, 0x0a, 0x07, 0x62, 0x65, - 0x64, 0x72, 0x6f, 0x63, 0x6b, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4b, 0x69, - 0x6e, 0x64, 0x42, 0x65, 0x64, 0x72, 0x6f, 0x63, 0x6b, 0x52, 0x07, 0x62, 0x65, 0x64, 0x72, 0x6f, - 0x63, 0x6b, 0x22, 0xf4, 0x02, 0x0a, 0x15, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, - 0x72, 0x4b, 0x69, 0x6e, 0x64, 0x42, 0x65, 0x64, 0x72, 0x6f, 0x63, 0x6b, 0x12, 0x16, 0x0a, 0x06, - 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, - 0x67, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6b, - 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x4b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x11, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6b, 0x65, - 0x79, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, - 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, - 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x28, 0x0a, 0x10, 0x73, 0x6d, 0x61, 0x6c, 0x6c, 0x5f, 0x66, - 0x61, 0x73, 0x74, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0e, 0x73, 0x6d, 0x61, 0x6c, 0x6c, 0x46, 0x61, 0x73, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x12, - 0x19, 0x0a, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x61, 0x72, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x07, 0x72, 0x6f, 0x6c, 0x65, 0x41, 0x72, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x78, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0a, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x25, 0x0a, 0x0e, 0x72, 0x65, 0x73, 0x6f, 0x6c, - 0x76, 0x65, 0x64, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x39, - 0x0a, 0x19, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x5f, 0x73, 0x6d, 0x61, 0x6c, 0x6c, - 0x5f, 0x66, 0x61, 0x73, 0x74, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x0a, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x16, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x53, 0x6d, 0x61, 0x6c, 0x6c, - 0x46, 0x61, 0x73, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x32, 0xa9, 0x04, 0x0a, 0x08, 0x52, 0x65, - 0x63, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x59, 0x0a, 0x12, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, - 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, - 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, - 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x68, 0x0a, 0x17, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, - 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x12, 0x25, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, - 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, - 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, - 0x64, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x10, 0x52, - 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, - 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x56, 0x0a, 0x11, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, - 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, - 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, + 0x12, 0x35, 0x0a, 0x17, 0x63, 0x61, 0x63, 0x68, 0x65, 0x5f, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x69, + 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x14, 0x63, 0x61, 0x63, 0x68, 0x65, 0x52, 0x65, 0x61, 0x64, 0x49, 0x6e, 0x70, 0x75, + 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x37, 0x0a, 0x18, 0x63, 0x61, 0x63, 0x68, 0x65, + 0x5f, 0x77, 0x72, 0x69, 0x74, 0x65, 0x5f, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x15, 0x63, 0x61, 0x63, 0x68, 0x65, + 0x57, 0x72, 0x69, 0x74, 0x65, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, + 0x1a, 0x51, 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, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, + 0x02, 0x38, 0x01, 0x22, 0x1a, 0x0a, 0x18, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0xcb, 0x02, 0x0a, 0x18, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, + 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x73, 0x67, 0x5f, 0x69, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, 0x67, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, + 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, + 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x49, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x50, 0x0a, 0x0f, 0x52, 0x65, 0x63, 0x6f, - 0x72, 0x64, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1d, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, - 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, - 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x59, 0x0a, 0x12, 0x52, 0x65, - 0x63, 0x6f, 0x72, 0x64, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x54, 0x68, 0x6f, 0x75, 0x67, 0x68, 0x74, - 0x12, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4d, - 0x6f, 0x64, 0x65, 0x6c, 0x54, 0x68, 0x6f, 0x75, 0x67, 0x68, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, - 0x64, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x54, 0x68, 0x6f, 0x75, 0x67, 0x68, 0x74, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xeb, 0x01, 0x0a, 0x0f, 0x4d, 0x43, 0x50, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x5c, 0x0a, 0x13, 0x47, 0x65, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 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, + 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x05, 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, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x1a, 0x51, 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, 0x2a, 0x0a, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, + 0x6e, 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x1b, 0x0a, + 0x19, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, + 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xa8, 0x04, 0x0a, 0x16, 0x52, + 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x15, + 0x0a, 0x06, 0x6d, 0x73, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x6d, 0x73, 0x67, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, + 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x09, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x6f, 0x6f, + 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x12, 0x14, 0x0a, + 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6e, + 0x70, 0x75, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, + 0x2e, 0x0a, 0x10, 0x69, 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x0f, 0x69, 0x6e, 0x76, + 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x88, 0x01, 0x01, 0x12, + 0x47, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x08, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x2b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, + 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 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, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x09, 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, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x64, 0x41, 0x74, 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x6f, 0x6f, 0x6c, 0x5f, 0x63, 0x61, 0x6c, 0x6c, + 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x6f, 0x6f, 0x6c, 0x43, + 0x61, 0x6c, 0x6c, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x69, 0x64, + 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x69, 0x74, 0x65, 0x6d, 0x49, 0x64, 0x1a, 0x51, + 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, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, + 0x01, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, + 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x69, 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x19, 0x0a, 0x17, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, + 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0xb8, 0x02, 0x0a, 0x19, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4d, 0x6f, 0x64, 0x65, 0x6c, + 0x54, 0x68, 0x6f, 0x75, 0x67, 0x68, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, + 0x0a, 0x0f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, + 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, + 0x74, 0x12, 0x4a, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, + 0x72, 0x64, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x54, 0x68, 0x6f, 0x75, 0x67, 0x68, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 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, 0x39, 0x0a, + 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x04, 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, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x1a, 0x51, 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, 0x2a, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x1c, 0x0a, 0x1a, 0x52, + 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x54, 0x68, 0x6f, 0x75, 0x67, 0x68, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x35, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, - 0x12, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, - 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7a, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x4d, 0x43, - 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, - 0x65, 0x6e, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x12, 0x2b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, + 0x22, 0xb2, 0x01, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x40, 0x0a, 0x10, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x6d, 0x63, 0x70, 0x5f, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x52, 0x0e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x4d, 0x63, 0x70, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x12, 0x51, 0x0a, 0x19, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, + 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x63, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4d, 0x43, + 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x16, 0x65, + 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x63, 0x70, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x73, 0x22, 0x85, 0x01, 0x0a, 0x0f, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x28, 0x0a, 0x10, 0x74, + 0x6f, 0x6f, 0x6c, 0x5f, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x65, 0x67, 0x65, 0x78, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x74, 0x6f, 0x6f, 0x6c, 0x41, 0x6c, 0x6c, 0x6f, 0x77, + 0x52, 0x65, 0x67, 0x65, 0x78, 0x12, 0x26, 0x0a, 0x0f, 0x74, 0x6f, 0x6f, 0x6c, 0x5f, 0x64, 0x65, + 0x6e, 0x79, 0x5f, 0x72, 0x65, 0x67, 0x65, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, + 0x74, 0x6f, 0x6f, 0x6c, 0x44, 0x65, 0x6e, 0x79, 0x52, 0x65, 0x67, 0x65, 0x78, 0x22, 0x72, 0x0a, + 0x24, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, - 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x32, 0xaa, 0x01, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, - 0x65, 0x72, 0x12, 0x47, 0x0a, 0x0c, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, - 0x65, 0x64, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x73, 0x41, 0x75, 0x74, - 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, - 0x7a, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x10, 0x49, - 0x73, 0x42, 0x75, 0x64, 0x67, 0x65, 0x74, 0x45, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x12, - 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x73, 0x42, 0x75, 0x64, 0x67, 0x65, 0x74, - 0x45, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x73, 0x42, 0x75, 0x64, 0x67, 0x65, 0x74, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x31, + 0x0a, 0x15, 0x6d, 0x63, 0x70, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x6d, + 0x63, 0x70, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x49, 0x64, + 0x73, 0x22, 0xda, 0x02, 0x0a, 0x25, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, + 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x0d, 0x61, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x3e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, + 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x2e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x52, 0x0c, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, + 0x12, 0x50, 0x0a, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x38, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x45, + 0x72, 0x72, 0x6f, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x73, 0x1a, 0x3f, 0x0a, 0x11, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 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, 0x1a, 0x39, 0x0a, 0x0b, 0x45, 0x72, 0x72, 0x6f, 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, 0x3e, + 0x0a, 0x13, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x22, 0x8e, + 0x01, 0x0a, 0x14, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x77, 0x6e, 0x65, 0x72, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x77, 0x6e, 0x65, 0x72, + 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x0a, 0x61, 0x70, 0x69, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x49, 0x64, + 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, + 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x22, + 0x32, 0x0a, 0x17, 0x49, 0x73, 0x42, 0x75, 0x64, 0x67, 0x65, 0x74, 0x45, 0x78, 0x63, 0x65, 0x65, + 0x64, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, + 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, + 0x72, 0x49, 0x64, 0x22, 0x80, 0x01, 0x0a, 0x18, 0x49, 0x73, 0x42, 0x75, 0x64, 0x67, 0x65, 0x74, 0x45, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x32, 0xbc, 0x01, 0x0a, 0x14, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x4d, 0x0a, 0x0e, 0x47, 0x65, 0x74, - 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x1c, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, - 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x10, 0x57, 0x61, 0x74, 0x63, - 0x68, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x1e, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, - 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, - 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x42, - 0x32, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, - 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x64, 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x08, 0x65, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x12, 0x31, 0x0a, 0x12, + 0x73, 0x70, 0x65, 0x6e, 0x64, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x5f, 0x6d, 0x69, 0x63, 0x72, + 0x6f, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x10, 0x73, 0x70, 0x65, 0x6e, + 0x64, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x88, 0x01, 0x01, 0x42, + 0x15, 0x0a, 0x13, 0x5f, 0x73, 0x70, 0x65, 0x6e, 0x64, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x5f, + 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x22, 0x17, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x41, 0x49, 0x50, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, + 0x49, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x09, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, + 0x09, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x22, 0x19, 0x0a, 0x17, 0x57, 0x61, + 0x74, 0x63, 0x68, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x1a, 0x0a, 0x18, 0x57, 0x61, 0x74, 0x63, 0x68, 0x41, 0x49, + 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0xb5, 0x01, 0x0a, 0x0a, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x73, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x12, 0x0a, + 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x65, 0x79, + 0x73, 0x12, 0x36, 0x0a, 0x07, 0x62, 0x65, 0x64, 0x72, 0x6f, 0x63, 0x6b, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x49, 0x50, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x4b, 0x69, 0x6e, 0x64, 0x42, 0x65, 0x64, 0x72, 0x6f, 0x63, 0x6b, + 0x52, 0x07, 0x62, 0x65, 0x64, 0x72, 0x6f, 0x63, 0x6b, 0x22, 0xf4, 0x02, 0x0a, 0x15, 0x41, 0x49, + 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4b, 0x69, 0x6e, 0x64, 0x42, 0x65, 0x64, 0x72, + 0x6f, 0x63, 0x6b, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x61, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x11, 0x61, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, + 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x28, 0x0a, 0x10, + 0x73, 0x6d, 0x61, 0x6c, 0x6c, 0x5f, 0x66, 0x61, 0x73, 0x74, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x6c, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x6d, 0x61, 0x6c, 0x6c, 0x46, 0x61, 0x73, + 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x19, 0x0a, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x61, + 0x72, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x72, 0x6f, 0x6c, 0x65, 0x41, 0x72, + 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x69, 0x64, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x08, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x25, + 0x0a, 0x0e, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x6c, + 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, + 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x39, 0x0a, 0x19, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, + 0x64, 0x5f, 0x73, 0x6d, 0x61, 0x6c, 0x6c, 0x5f, 0x66, 0x61, 0x73, 0x74, 0x5f, 0x6d, 0x6f, 0x64, + 0x65, 0x6c, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, + 0x65, 0x64, 0x53, 0x6d, 0x61, 0x6c, 0x6c, 0x46, 0x61, 0x73, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x6c, + 0x32, 0xa9, 0x04, 0x0a, 0x08, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x59, 0x0a, + 0x12, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, + 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, + 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x68, 0x0a, 0x17, 0x52, 0x65, 0x63, 0x6f, + 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, + 0x64, 0x65, 0x64, 0x12, 0x25, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, + 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, + 0x64, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x53, 0x0a, 0x10, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, + 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, + 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x11, 0x52, 0x65, 0x63, 0x6f, 0x72, + 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1f, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, + 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d, + 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x50, 0x0a, 0x0f, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, + 0x67, 0x65, 0x12, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, + 0x64, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, + 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x59, 0x0a, 0x12, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4d, 0x6f, 0x64, 0x65, 0x6c, + 0x54, 0x68, 0x6f, 0x75, 0x67, 0x68, 0x74, 0x12, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x54, 0x68, 0x6f, 0x75, 0x67, + 0x68, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x54, 0x68, 0x6f, + 0x75, 0x67, 0x68, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xeb, 0x01, 0x0a, + 0x0f, 0x4d, 0x43, 0x50, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x6f, 0x72, + 0x12, 0x5c, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x12, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7a, + 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x12, + 0x2b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, + 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, 0x74, + 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xaa, 0x01, 0x0a, 0x0a, 0x41, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x72, 0x12, 0x47, 0x0a, 0x0c, 0x49, 0x73, 0x41, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x73, + 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x53, 0x0a, 0x10, 0x49, 0x73, 0x42, 0x75, 0x64, 0x67, 0x65, 0x74, 0x45, 0x78, + 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, + 0x73, 0x42, 0x75, 0x64, 0x67, 0x65, 0x74, 0x45, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, + 0x73, 0x42, 0x75, 0x64, 0x67, 0x65, 0x74, 0x45, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xbc, 0x01, 0x0a, 0x14, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x6f, 0x72, + 0x12, 0x4d, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x73, 0x12, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x49, + 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x49, 0x50, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x55, 0x0a, 0x10, 0x57, 0x61, 0x74, 0x63, 0x68, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x73, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x57, 0x61, 0x74, 0x63, + 0x68, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x57, 0x61, 0x74, 0x63, + 0x68, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x42, 0x32, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2f, 0x76, 0x32, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x64, 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, + 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( diff --git a/coderd/aibridged/proto/aibridged.proto b/coderd/aibridged/proto/aibridged.proto index dcfc1db46d2..1e8abd1d4b1 100644 --- a/coderd/aibridged/proto/aibridged.proto +++ b/coderd/aibridged/proto/aibridged.proto @@ -75,6 +75,7 @@ message RecordInterceptionRequest { // used to order network requests relative to Agent Firewall audit events. // Absent when the request did not pass through Agent Firewall. optional int32 agent_firewall_sequence_number = 16; + string workspace_id = 17; // UUID. Empty when workspace attribution is unknown. } message RecordInterceptionResponse {} @@ -185,6 +186,7 @@ message IsAuthorizedResponse { string owner_id = 1; string api_key_id = 2; string username = 3; + string workspace_id = 4; // UUID. Empty when workspace attribution is unknown. } message IsBudgetExceededRequest { diff --git a/coderd/aibridged/proto/version.go b/coderd/aibridged/proto/version.go index 631ba3af06c..876ca23a193 100644 --- a/coderd/aibridged/proto/version.go +++ b/coderd/aibridged/proto/version.go @@ -17,9 +17,12 @@ import "github.com/coder/coder/v2/apiversion" // - Adds the ProviderConfigurator.WatchAIProviders streaming RPC, pushing a // change signal to gateways so a running standalone gateway refetches its // provider set when the provider configuration changes. +// +// API v1.3: +// - Adds workspace_id to RecordInterceptionRequest and IsAuthorizedResponse. const ( CurrentMajor = 1 - CurrentMinor = 2 + CurrentMinor = 3 ) // VersionQueryParam is the URL query parameter the standalone AI Gateway diff --git a/coderd/aibridgedserver/aibridgedserver.go b/coderd/aibridgedserver/aibridgedserver.go index a82fdfc28a5..672e8625947 100644 --- a/coderd/aibridgedserver/aibridgedserver.go +++ b/coderd/aibridgedserver/aibridgedserver.go @@ -31,6 +31,7 @@ import ( "github.com/coder/coder/v2/coderd/httpmw" codermcp "github.com/coder/coder/v2/coderd/mcp" "github.com/coder/coder/v2/coderd/notifications" + "github.com/coder/coder/v2/coderd/provisionerdserver" coderdpubsub "github.com/coder/coder/v2/coderd/pubsub" "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" @@ -46,14 +47,15 @@ var ( // matching. // TODO: return these errors to the client in a more structured/comparable // way. - ErrInvalidKey = xerrors.New("invalid key") - ErrUnknownKey = xerrors.New("unknown key") - ErrExpired = xerrors.New("expired") - ErrUnknownUser = xerrors.New("unknown user") - ErrDeletedUser = xerrors.New("deleted user") - ErrInactiveUser = xerrors.New("inactive user") - ErrSystemUser = xerrors.New("system user") - ErrAmbiguousAuth = xerrors.New("both key and key_id set; exactly one required") + ErrInvalidKey = xerrors.New("invalid key") + ErrUnknownKey = xerrors.New("unknown key") + ErrExpired = xerrors.New("expired") + ErrUnknownUser = xerrors.New("unknown user") + ErrDeletedUser = xerrors.New("deleted user") + ErrInactiveUser = xerrors.New("inactive user") + ErrSystemUser = xerrors.New("system user") + ErrAmbiguousAuth = xerrors.New("both key and key_id set; exactly one required") + ErrWorkspaceAttribution = xerrors.New("invalid workspace attribution") ErrNoExternalAuthLinkFound = xerrors.New("no external auth link found") ) @@ -94,7 +96,6 @@ type store interface { // Authorizer-related queries. GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) GetUserByID(ctx context.Context, id uuid.UUID) (database.User, error) - // ProviderConfigurator-related queries. InTx wraps the provider and key // reads in a single read-only transaction. GetAIProviders(ctx context.Context, arg database.GetAIProvidersParams) ([]database.AIProvider, error) @@ -223,8 +224,29 @@ func (s *Server) RecordInterception(ctx context.Context, in *proto.RecordInterce // Look up the interception lineage using the correlating tool call ID. parentID, rootID := s.findInterceptionLineage(ctx, in.GetCorrelatingToolCallId()) + out, err := json.Marshal(metadata) + if err != nil { + s.logger.Warn(ctx, "failed to marshal ai gateway metadata from proto to JSON", slog.F("metadata", in), slog.Error(err)) + } + + providerName := strings.TrimSpace(in.ProviderName) + if providerName == "" { + providerName = in.Provider + } + + agentFirewallSessionID, err := parseOptionalUUID(in.AgentFirewallSessionId) + if err != nil { + s.logger.Warn(ctx, "invalid agent firewall session ID in interception request", + slog.F("agent_firewall_session_id", in.GetAgentFirewallSessionId()), slog.Error(err)) + } + + workspaceID, err := interceptionAttribution(in) + if err != nil { + s.logger.Warn(ctx, "failed to parse interception attribution", slog.F("metadata", in), slog.Error(err)) + } + if s.structuredLogging { - s.logger.Info(ctx, InterceptionLogMarker, + fields := []slog.Field{ slog.F("record_type", "interception_start"), slog.F("interception_id", intcID.String()), slog.F("initiator_id", initID.String()), @@ -238,23 +260,11 @@ func (s *Server) RecordInterception(ctx context.Context, in *proto.RecordInterce slog.F("correlating_tool_call_id", in.GetCorrelatingToolCallId()), slog.F("thread_parent_id", parentID), slog.F("thread_root_id", rootID), - ) - } - - out, err := json.Marshal(metadata) - if err != nil { - s.logger.Warn(ctx, "failed to marshal aibridge metadata from proto to JSON", slog.F("metadata", in), slog.Error(err)) - } - - providerName := strings.TrimSpace(in.ProviderName) - if providerName == "" { - providerName = in.Provider - } - - agentFirewallSessionID, err := parseOptionalUUID(in.AgentFirewallSessionId) - if err != nil { - s.logger.Warn(ctx, "invalid agent firewall session ID in interception request", - slog.F("agent_firewall_session_id", in.GetAgentFirewallSessionId()), slog.Error(err)) + } + if workspaceID.Valid { + fields = append(fields, slog.F("workspace_id", workspaceID.UUID)) + } + s.logger.Info(ctx, InterceptionLogMarker, fields...) } _, err = s.store.InsertAIBridgeInterception(ctx, database.InsertAIBridgeInterceptionParams{ @@ -274,6 +284,7 @@ func (s *Server) RecordInterception(ctx context.Context, in *proto.RecordInterce CredentialHint: in.CredentialHint, AgentFirewallSessionID: agentFirewallSessionID, AgentFirewallSequenceNumber: parseOptionalInt32(in.AgentFirewallSequenceNumber), + WorkspaceID: workspaceID, }) if err != nil { return nil, xerrors.Errorf("start interception: %w", err) @@ -820,11 +831,55 @@ func (s *Server) IsAuthorized(ctx context.Context, in *proto.IsAuthorizedRequest return nil, ErrSystemUser } - return &proto.IsAuthorizedResponse{ + resp := &proto.IsAuthorizedResponse{ OwnerId: key.UserID.String(), ApiKeyId: key.ID, Username: user.Username, - }, nil + } + if !delegated { + workspaceID, ok, err := workspaceAttribution(key) + if err != nil { + return nil, err + } + if ok { + resp.WorkspaceId = workspaceID.String() + } + } + return resp, nil +} + +// workspaceAttribution parses the workspace UUID from a strict server-minted +// token name without performing any database lookup. It returns the workspace +// UUID and true when the token name matches the expected pattern and the +// embedded owner UUID equals key.UserID. It fails closed (ErrWorkspaceAttribution) +// when the embedded owner does not match. +func workspaceAttribution(key database.APIKey) (uuid.UUID, bool, error) { + if key.LoginType == database.LoginTypeToken { + return uuid.Nil, false, nil + } + + ownerID, workspaceID, ok := provisionerdserver.ParseWorkspaceSessionTokenName(key.TokenName) + if !ok { + return uuid.Nil, false, nil + } + if ownerID != key.UserID { + return uuid.Nil, false, ErrWorkspaceAttribution + } + return workspaceID, true, nil +} + +// interceptionAttribution parses the optional workspace_id from the +// interception request. The returned NullUUID is valid only when a non-empty +// workspace_id was provided. +func interceptionAttribution(in *proto.RecordInterceptionRequest) (workspaceID uuid.NullUUID, err error) { + if in.GetWorkspaceId() == "" { + return uuid.NullUUID{}, nil + } + id, err := uuid.Parse(in.GetWorkspaceId()) + if err != nil { + return uuid.NullUUID{}, xerrors.Errorf("invalid workspace ID: %w", err) + } + return uuid.NullUUID{UUID: id, Valid: true}, nil } // IsBudgetExceeded reports whether the user's AI spend has reached their diff --git a/coderd/aibridgedserver/aibridgedserver_test.go b/coderd/aibridgedserver/aibridgedserver_test.go index c55df5f843c..02b58dfceff 100644 --- a/coderd/aibridgedserver/aibridgedserver_test.go +++ b/coderd/aibridgedserver/aibridgedserver_test.go @@ -416,6 +416,102 @@ func TestAuthorization_Delegated(t *testing.T) { } } +func TestAuthorization_WorkspaceAttribution(t *testing.T) { + t.Parallel() + + // workspaceAttribution is lookup-free: it parses the workspace UUID from the + // strict server-minted token name and validates the embedded owner against + // key.UserID without touching the database. + tests := []struct { + name string + configure func(key *database.APIKey, user database.User, workspaceID uuid.UUID) + wantErr error + wantWsID bool // expect resp.GetWorkspaceId() == workspaceID.String() + }{ + { + name: "valid", + configure: func(key *database.APIKey, user database.User, workspaceID uuid.UUID) { + key.TokenName = fmt.Sprintf("%s_%s_session_token", user.ID, workspaceID) + }, + wantWsID: true, + }, + { + // LoginType == Token must never yield workspace attribution even when + // the token name matches the strict pattern. + name: "personal token spoof", + configure: func(key *database.APIKey, user database.User, workspaceID uuid.UUID) { + key.LoginType = database.LoginTypeToken + key.TokenName = fmt.Sprintf("%s_%s_session_token", user.ID, workspaceID) + }, + }, + { + // A token name that matches the suffix but not the strict UUID-pair + // prefix must not be attributed to any workspace. + name: "chatd lookalike", + configure: func(key *database.APIKey, user database.User, _ uuid.UUID) { + key.TokenName = fmt.Sprintf("chatd_%s_session_token", user.ID) + }, + }, + { + // An extra word between the UUIDs does not match the strict pattern. + name: "oauth lookalike", + configure: func(key *database.APIKey, user database.User, workspaceID uuid.UUID) { + key.TokenName = fmt.Sprintf("%s_%s_oauth_session_token", user.ID, workspaceID) + }, + }, + { + // The embedded owner UUID in the token name differs from key.UserID. + // Fail closed: return ErrWorkspaceAttribution. + name: "embedded owner mismatch", + configure: func(key *database.APIKey, _ database.User, workspaceID uuid.UUID) { + key.TokenName = fmt.Sprintf("%s_%s_session_token", uuid.New(), workspaceID) + }, + wantErr: aibridgedserver.ErrWorkspaceAttribution, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + db := dbmock.NewMockStore(ctrl) + now := dbtime.Now() + user := database.User{ID: uuid.New(), Username: "test", Status: database.UserStatusActive, LoginType: database.LoginTypePassword} + workspaceID := uuid.New() + keyID, err := cryptorand.String(10) + require.NoError(t, err) + secret, hashedSecret, err := apikey.GenerateSecret(22) + require.NoError(t, err) + key := database.APIKey{ID: keyID, UserID: user.ID, HashedSecret: hashedSecret, ExpiresAt: now.Add(time.Hour), LoginType: database.LoginTypePassword} + tt.configure(&key, user, workspaceID) + + db.EXPECT().GetAPIKeyByID(gomock.Any(), key.ID).Return(key, nil) + db.EXPECT().GetUserByID(gomock.Any(), user.ID).Return(user, nil) + // No GetWorkspaceByID: attribution is lookup-free. + + srv, err := aibridgedserver.NewServer(t.Context(), aibridgedserver.Options{ + Store: db, AISeatTracker: agplaiseats.Noop{}, AccessURL: "/", + GatewayCfg: codersdk.AIBridgeConfig{}, Experiments: requiredExperiments, + Logger: testutil.Logger(t), Clock: quartz.NewReal(), + }) + require.NoError(t, err) + + resp, err := srv.IsAuthorized(t.Context(), &proto.IsAuthorizedRequest{Key: key.ID + "-" + secret}) + if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) + return + } + require.NoError(t, err) + if tt.wantWsID { + require.Equal(t, workspaceID.String(), resp.GetWorkspaceId()) + } else { + require.Empty(t, resp.GetWorkspaceId()) + } + }) + } +} + func TestIsBudgetExceeded(t *testing.T) { t.Parallel() diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index cc3968fe62b..a9b72586a2e 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -17592,6 +17592,18 @@ const docTemplate = `{ "codersdk.AIBridgeAgenticAction": { "type": "object", "properties": { + "attribution": { + "description": "Attribution contains attribution data from this interception.\nUnknown attribution is serialized as an empty object.", + "allOf": [ + { + "$ref": "#/definitions/codersdk.AIBridgeAttribution" + } + ] + }, + "interception_id": { + "type": "string", + "format": "uuid" + }, "model": { "type": "string" }, @@ -17612,6 +17624,12 @@ const docTemplate = `{ } } }, + "codersdk.AIBridgeAttribution": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, "codersdk.AIBridgeConfig": { "type": "object", "properties": { @@ -17972,6 +17990,14 @@ const docTemplate = `{ "$ref": "#/definitions/codersdk.AIBridgeAgenticAction" } }, + "attribution": { + "description": "Attribution contains attribution data from the root interception.\nUnknown attribution is serialized as an empty object.", + "allOf": [ + { + "$ref": "#/definitions/codersdk.AIBridgeAttribution" + } + ] + }, "credential_hint": { "type": "string" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 2f0c58be297..0dd3ed287de 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -15662,6 +15662,18 @@ "codersdk.AIBridgeAgenticAction": { "type": "object", "properties": { + "attribution": { + "description": "Attribution contains attribution data from this interception.\nUnknown attribution is serialized as an empty object.", + "allOf": [ + { + "$ref": "#/definitions/codersdk.AIBridgeAttribution" + } + ] + }, + "interception_id": { + "type": "string", + "format": "uuid" + }, "model": { "type": "string" }, @@ -15682,6 +15694,12 @@ } } }, + "codersdk.AIBridgeAttribution": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, "codersdk.AIBridgeConfig": { "type": "object", "properties": { @@ -16042,6 +16060,14 @@ "$ref": "#/definitions/codersdk.AIBridgeAgenticAction" } }, + "attribution": { + "description": "Attribution contains attribution data from the root interception.\nUnknown attribution is serialized as an empty object.", + "allOf": [ + { + "$ref": "#/definitions/codersdk.AIBridgeAttribution" + } + ] + }, "credential_hint": { "type": "string" }, diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index 1e033a14abe..b8e259f3c74 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -1387,13 +1387,13 @@ func buildAIBridgeThread( } } - // Build agentic actions grouped by interception. Each interception that - // has tool calls produces one action with all its tool calls, thinking - // blocks, and token usage. + // Build one action per interception. Child interceptions always produce + // an action, including when they have no tool calls. The root action is + // emitted only when the actual thread-root interception has tool calls. var actions []codersdk.AIBridgeAgenticAction for _, intc := range interceptions { tools := toolsByInterception[intc.ID] - if len(tools) == 0 { + if intc.ID == threadID && len(tools) == 0 { continue } @@ -1426,10 +1426,12 @@ func buildAIBridgeThread( } actions = append(actions, codersdk.AIBridgeAgenticAction{ - Model: intc.Model, - TokenUsage: actionTokenUsage, - Thinking: thinking, - ToolCalls: toolCalls, + InterceptionID: intc.ID, + Model: intc.Model, + Attribution: aiBridgeInterceptionAttribution(intc), + TokenUsage: actionTokenUsage, + Thinking: thinking, + ToolCalls: toolCalls, }) } @@ -1440,6 +1442,10 @@ func buildAIBridgeThread( thread.AgenticActions = actions + if rootIntc != nil && rootIntc.ID == threadID { + thread.Attribution = aiBridgeInterceptionAttribution(*rootIntc) + } + // Aggregate thread-level token usage. var threadTokens []database.AIBridgeTokenUsage for _, intc := range interceptions { @@ -1599,6 +1605,17 @@ func InvalidatedPresets(invalidatedPresets []database.UpdatePresetsLastInvalidat return presets } +// aiBridgeInterceptionAttribution builds attribution from the dedicated +// interception columns. It returns nil when no workspace context is recorded. +func aiBridgeInterceptionAttribution(intc database.AIBridgeInterception) codersdk.AIBridgeAttribution { + if !intc.WorkspaceID.Valid { + return nil + } + return codersdk.AIBridgeAttribution{ + "workspace_id": intc.WorkspaceID.UUID.String(), + } +} + // sanitizeCredentialHint ensures the hint looks masked before exposing // it in the API. The aibridge library uses "..." as the masking // delimiter (e.g. "sk-a...efgh"), so we check for its presence. If diff --git a/coderd/database/db2sdk/db2sdk_internal_test.go b/coderd/database/db2sdk/db2sdk_internal_test.go index e7492eaa6a5..110444ea213 100644 --- a/coderd/database/db2sdk/db2sdk_internal_test.go +++ b/coderd/database/db2sdk/db2sdk_internal_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/codersdk" ) func TestAggregateTokenMetadata(t *testing.T) { @@ -332,3 +333,259 @@ func TestSanitizeCredentialHint(t *testing.T) { }) } } + +// TestAIBridgeInterceptionAttribution covers unknown and workspace attribution. +func TestAIBridgeInterceptionAttribution(t *testing.T) { + t.Parallel() + + t.Run("nil_when_no_workspace", func(t *testing.T) { + t.Parallel() + intc := database.AIBridgeInterception{ + ID: uuid.New(), + // WorkspaceID is zero-value (not valid). + } + require.Nil(t, aiBridgeInterceptionAttribution(intc)) + }) + + t.Run("workspace_id_only", func(t *testing.T) { + t.Parallel() + wsID := uuid.New() + intc := database.AIBridgeInterception{ + ID: uuid.New(), + WorkspaceID: uuid.NullUUID{UUID: wsID, Valid: true}, + } + want := codersdk.AIBridgeAttribution{ + "workspace_id": wsID.String(), + } + require.Equal(t, want, aiBridgeInterceptionAttribution(intc)) + }) +} + +// TestBuildAIBridgeThreadAttributionAndActions verifies that attribution comes +// from the root only and that non-root interceptions become actions, including +// tool-less interceptions. +func TestBuildAIBridgeThreadAttributionAndActions(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + rootWorkspace uuid.NullUUID + wantRoot codersdk.AIBridgeAttribution + }{ + { + name: "known_root", + rootWorkspace: uuid.NullUUID{ + UUID: uuid.MustParse("11111111-1111-1111-1111-111111111111"), + Valid: true, + }, + wantRoot: codersdk.AIBridgeAttribution{ + "workspace_id": "11111111-1111-1111-1111-111111111111", + }, + }, + { + name: "unknown_root", + wantRoot: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + threadID := uuid.MustParse("00000000-0000-0000-0000-000000000001") + childWithWorkspaceID := uuid.MustParse("00000000-0000-0000-0000-000000000002") + toolLessChildID := uuid.MustParse("00000000-0000-0000-0000-000000000003") + childWorkspaceID := uuid.MustParse("22222222-2222-2222-2222-222222222222") + rootTools := []database.AIBridgeToolUsage{ + { + ID: uuid.MustParse("00000000-0000-0000-0000-000000000010"), + InterceptionID: threadID, + Tool: "root_tool", + }, + } + interceptions := []database.AIBridgeInterception{ + { + // The root ID must match threadID. Attribution on children must + // not replace this value. + ID: threadID, + Model: "root-model", + Provider: "openai", + WorkspaceID: tt.rootWorkspace, + }, + { + ID: childWithWorkspaceID, + Model: "child-model", + Provider: "openai", + WorkspaceID: uuid.NullUUID{UUID: childWorkspaceID, Valid: true}, + }, + { + ID: toolLessChildID, + Model: "tool-less-child-model", + Provider: "openai", + }, + } + + thread := buildAIBridgeThread( + threadID, + interceptions, + make(map[uuid.UUID][]database.AIBridgeTokenUsage), + map[uuid.UUID][]database.AIBridgeToolUsage{ + threadID: rootTools, + }, + make(map[uuid.UUID][]database.AIBridgeUserPrompt), + make(map[uuid.UUID][]database.AIBridgeModelThought), + ) + + require.Equal(t, tt.wantRoot, thread.Attribution) + require.Len(t, thread.AgenticActions, 3) + + rootAction := thread.AgenticActions[0] + require.Equal(t, threadID, rootAction.InterceptionID) + require.Equal(t, tt.wantRoot, rootAction.Attribution) + require.Len(t, rootAction.ToolCalls, 1) + require.Equal(t, "root_tool", rootAction.ToolCalls[0].Tool) + + childAction := thread.AgenticActions[1] + require.Equal(t, childWithWorkspaceID, childAction.InterceptionID) + want := codersdk.AIBridgeAttribution{ + "workspace_id": childWorkspaceID.String(), + } + require.Equal(t, want, childAction.Attribution) + require.Empty(t, childAction.ToolCalls) + require.NotNil(t, childAction.ToolCalls) + + toolLessChildAction := thread.AgenticActions[2] + require.Equal(t, toolLessChildID, toolLessChildAction.InterceptionID) + require.Empty(t, toolLessChildAction.Attribution) + require.Empty(t, toolLessChildAction.ToolCalls) + require.NotNil(t, toolLessChildAction.ToolCalls) + + var raw map[string]json.RawMessage + jsonBytes, err := json.Marshal(thread) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(jsonBytes, &raw)) + require.NotContains(t, raw, "interception_attributions") + wantRootJSON, err := json.Marshal(tt.wantRoot) + require.NoError(t, err) + require.JSONEq(t, string(wantRootJSON), string(raw["attribution"])) + if tt.wantRoot == nil { + require.JSONEq(t, `{}`, string(raw["attribution"])) + } + + var rawActions []struct { + Attribution json.RawMessage `json:"attribution"` + } + require.NoError(t, json.Unmarshal(raw["agentic_actions"], &rawActions)) + require.JSONEq(t, `{}`, string(rawActions[2].Attribution)) + require.NotContains(t, string(raw["attribution"]), childWithWorkspaceID.String()) + require.NotContains(t, string(raw["attribution"]), toolLessChildID.String()) + }) + } +} + +func TestBuildAIBridgeThreadMissingRoot(t *testing.T) { + t.Parallel() + + threadID := uuid.MustParse("00000000-0000-0000-0000-000000000101") + childID := uuid.MustParse("00000000-0000-0000-0000-000000000102") + childWorkspaceID := uuid.MustParse("33333333-3333-3333-3333-333333333333") + + thread := buildAIBridgeThread( + threadID, + []database.AIBridgeInterception{ + { + // This is the fallback interception, not the actual root. + ID: childID, + Model: "child-model", + Provider: "openai", + WorkspaceID: uuid.NullUUID{UUID: childWorkspaceID, Valid: true}, + }, + }, + make(map[uuid.UUID][]database.AIBridgeTokenUsage), + make(map[uuid.UUID][]database.AIBridgeToolUsage), + make(map[uuid.UUID][]database.AIBridgeUserPrompt), + make(map[uuid.UUID][]database.AIBridgeModelThought), + ) + + require.Empty(t, thread.Attribution) + require.Len(t, thread.AgenticActions, 1) + require.Equal(t, childID, thread.AgenticActions[0].InterceptionID) + want := codersdk.AIBridgeAttribution{ + "workspace_id": childWorkspaceID.String(), + } + require.Equal(t, want, thread.AgenticActions[0].Attribution) + require.Empty(t, thread.AgenticActions[0].ToolCalls) + require.NotNil(t, thread.AgenticActions[0].ToolCalls) + + var raw struct { + Attribution json.RawMessage `json:"attribution"` + AgenticActions []struct { + Attribution json.RawMessage `json:"attribution"` + } `json:"agentic_actions"` + } + jsonBytes, err := json.Marshal(thread) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(jsonBytes, &raw)) + require.JSONEq(t, `{}`, string(raw.Attribution)) +} + +func TestBuildAIBridgeThreadStandaloneRootActions(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + workspaceID uuid.NullUUID + wantAttribution codersdk.AIBridgeAttribution + }{ + { + name: "known_root", + workspaceID: uuid.NullUUID{ + UUID: uuid.MustParse("44444444-4444-4444-4444-444444444444"), + Valid: true, + }, + wantAttribution: codersdk.AIBridgeAttribution{ + "workspace_id": "44444444-4444-4444-4444-444444444444", + }, + }, + {name: "unknown_root"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + threadID := uuid.MustParse("00000000-0000-0000-0000-000000000201") + thread := buildAIBridgeThread( + threadID, + []database.AIBridgeInterception{{ + ID: threadID, + Model: "root-model", + Provider: "openai", + WorkspaceID: tt.workspaceID, + }}, + make(map[uuid.UUID][]database.AIBridgeTokenUsage), + make(map[uuid.UUID][]database.AIBridgeToolUsage), + make(map[uuid.UUID][]database.AIBridgeUserPrompt), + make(map[uuid.UUID][]database.AIBridgeModelThought), + ) + + require.Equal(t, tt.wantAttribution, thread.Attribution) + require.Empty(t, thread.AgenticActions) + require.NotNil(t, thread.AgenticActions) + + var raw struct { + Attribution json.RawMessage `json:"attribution"` + } + jsonBytes, err := json.Marshal(thread) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(jsonBytes, &raw)) + if tt.wantAttribution == nil { + require.JSONEq(t, `{}`, string(raw.Attribution)) + } else { + wantJSON, err := json.Marshal(tt.wantAttribution) + require.NoError(t, err) + require.JSONEq(t, string(wantJSON), string(raw.Attribution)) + } + }) + } +} diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index 975e2ca32d3..1e04613f81a 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -2079,6 +2079,7 @@ func AIBridgeInterception(t testing.TB, db database.Store, seed database.InsertA CredentialHint: takeFirst(seed.CredentialHint, ""), AgentFirewallSessionID: seed.AgentFirewallSessionID, AgentFirewallSequenceNumber: seed.AgentFirewallSequenceNumber, + WorkspaceID: seed.WorkspaceID, }) if endedAt != nil { interception, err = db.UpdateAIBridgeInterceptionEnded(genCtx, database.UpdateAIBridgeInterceptionEndedParams{ diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 88ee0958020..c775af12548 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -1674,7 +1674,8 @@ CREATE TABLE aibridge_interceptions ( agent_firewall_session_id uuid, agent_firewall_sequence_number integer, error_type aibridge_interception_error_type, - error_message character varying(1024) + error_message character varying(1024), + workspace_id uuid ); COMMENT ON TABLE aibridge_interceptions IS 'Audit log of requests intercepted by AI Bridge'; @@ -1703,6 +1704,8 @@ COMMENT ON COLUMN aibridge_interceptions.error_type IS 'Categorised terminal ups COMMENT ON COLUMN aibridge_interceptions.error_message IS 'Raw terminal upstream error message for a failed interception; NULL when the interception succeeded.'; +COMMENT ON COLUMN aibridge_interceptions.workspace_id IS 'The workspace in which the agent ran. NULL when no workspace context is available.'; + CREATE TABLE aibridge_model_thoughts ( interception_id uuid NOT NULL, content text NOT NULL, diff --git a/coderd/database/migrations/000596_aibridge_workspace_context.down.sql b/coderd/database/migrations/000596_aibridge_workspace_context.down.sql new file mode 100644 index 00000000000..72f4a7e87e0 --- /dev/null +++ b/coderd/database/migrations/000596_aibridge_workspace_context.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE aibridge_interceptions + DROP COLUMN IF EXISTS workspace_id; diff --git a/coderd/database/migrations/000596_aibridge_workspace_context.up.sql b/coderd/database/migrations/000596_aibridge_workspace_context.up.sql new file mode 100644 index 00000000000..8b48d394d38 --- /dev/null +++ b/coderd/database/migrations/000596_aibridge_workspace_context.up.sql @@ -0,0 +1,13 @@ +-- Attach optional workspace context to an AI Bridge interception. +-- Two valid states: +-- (1) NULL - workspace unknown (default, backwards-compatible) +-- (2) non-NULL - workspace ID recorded at interception time +-- +-- No foreign key constraint as historical attribution must survive deletion of +-- the attributed workspace. + +ALTER TABLE aibridge_interceptions + ADD COLUMN workspace_id UUID NULL; + +COMMENT ON COLUMN aibridge_interceptions.workspace_id IS + 'The workspace in which the agent ran. NULL when no workspace context is available.'; diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 6478e5f5e9e..f9db353a0da 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -1231,6 +1231,7 @@ func (q *sqlQuerier) ListAuthorizedAIBridgeSessionThreads(ctx context.Context, a &i.AIBridgeInterception.AgentFirewallSequenceNumber, &i.AIBridgeInterception.ErrorType, &i.AIBridgeInterception.ErrorMessage, + &i.AIBridgeInterception.WorkspaceID, ); err != nil { return nil, err } diff --git a/coderd/database/models.go b/coderd/database/models.go index 29ea53e0216..2d0fedb71e8 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -4790,6 +4790,8 @@ type AIBridgeInterception struct { ErrorType NullAIBridgeInterceptionErrorType `db:"error_type" json:"error_type"` // Raw terminal upstream error message for a failed interception; NULL when the interception succeeded. ErrorMessage sql.NullString `db:"error_message" json:"error_message"` + // The workspace in which the agent ran. NULL when no workspace context is available. + WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"` } // Audit log of model thinking in intercepted requests in AI Bridge diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index b0601f689b7..531e943a9a4 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1264,7 +1264,7 @@ func (q *sqlQuerier) GetAIBridgeChatCost(ctx context.Context, rootChatID uuid.UU const getAIBridgeInterceptionByID = `-- name: GetAIBridgeInterceptionByID :one SELECT - id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client, thread_parent_id, thread_root_id, client_session_id, session_id, provider_name, credential_kind, credential_hint, agent_firewall_session_id, agent_firewall_sequence_number, error_type, error_message + id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client, thread_parent_id, thread_root_id, client_session_id, session_id, provider_name, credential_kind, credential_hint, agent_firewall_session_id, agent_firewall_sequence_number, error_type, error_message, workspace_id FROM aibridge_interceptions WHERE @@ -1295,6 +1295,7 @@ func (q *sqlQuerier) GetAIBridgeInterceptionByID(ctx context.Context, id uuid.UU &i.AgentFirewallSequenceNumber, &i.ErrorType, &i.ErrorMessage, + &i.WorkspaceID, ) return i, err } @@ -1329,7 +1330,7 @@ func (q *sqlQuerier) GetAIBridgeInterceptionLineageByToolCallID(ctx context.Cont const getAIBridgeInterceptions = `-- name: GetAIBridgeInterceptions :many SELECT - id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client, thread_parent_id, thread_root_id, client_session_id, session_id, provider_name, credential_kind, credential_hint, agent_firewall_session_id, agent_firewall_sequence_number, error_type, error_message + id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client, thread_parent_id, thread_root_id, client_session_id, session_id, provider_name, credential_kind, credential_hint, agent_firewall_session_id, agent_firewall_sequence_number, error_type, error_message, workspace_id FROM aibridge_interceptions ` @@ -1364,6 +1365,7 @@ func (q *sqlQuerier) GetAIBridgeInterceptions(ctx context.Context) ([]AIBridgeIn &i.AgentFirewallSequenceNumber, &i.ErrorType, &i.ErrorMessage, + &i.WorkspaceID, ); err != nil { return nil, err } @@ -1614,11 +1616,11 @@ func (q *sqlQuerier) GetAIBridgeUserPromptsByInterceptionID(ctx context.Context, const insertAIBridgeInterception = `-- name: InsertAIBridgeInterception :one INSERT INTO aibridge_interceptions ( - id, api_key_id, initiator_id, provider, provider_name, model, metadata, started_at, client, client_session_id, thread_parent_id, thread_root_id, credential_kind, credential_hint, agent_firewall_session_id, agent_firewall_sequence_number + id, api_key_id, initiator_id, provider, provider_name, model, metadata, started_at, client, client_session_id, thread_parent_id, thread_root_id, credential_kind, credential_hint, agent_firewall_session_id, agent_firewall_sequence_number, workspace_id ) VALUES ( - $1, $2, $3, $4, $5, $6, COALESCE($7::jsonb, '{}'::jsonb), $8, $9, $10, $11::uuid, $12::uuid, $13, $14, $15::uuid, $16 + $1, $2, $3, $4, $5, $6, COALESCE($7::jsonb, '{}'::jsonb), $8, $9, $10, $11::uuid, $12::uuid, $13, $14, $15::uuid, $16, $17::uuid ) -RETURNING id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client, thread_parent_id, thread_root_id, client_session_id, session_id, provider_name, credential_kind, credential_hint, agent_firewall_session_id, agent_firewall_sequence_number, error_type, error_message +RETURNING id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client, thread_parent_id, thread_root_id, client_session_id, session_id, provider_name, credential_kind, credential_hint, agent_firewall_session_id, agent_firewall_sequence_number, error_type, error_message, workspace_id ` type InsertAIBridgeInterceptionParams struct { @@ -1638,6 +1640,7 @@ type InsertAIBridgeInterceptionParams struct { CredentialHint string `db:"credential_hint" json:"credential_hint"` AgentFirewallSessionID uuid.NullUUID `db:"agent_firewall_session_id" json:"agent_firewall_session_id"` AgentFirewallSequenceNumber sql.NullInt32 `db:"agent_firewall_sequence_number" json:"agent_firewall_sequence_number"` + WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"` } func (q *sqlQuerier) InsertAIBridgeInterception(ctx context.Context, arg InsertAIBridgeInterceptionParams) (AIBridgeInterception, error) { @@ -1658,6 +1661,7 @@ func (q *sqlQuerier) InsertAIBridgeInterception(ctx context.Context, arg InsertA arg.CredentialHint, arg.AgentFirewallSessionID, arg.AgentFirewallSequenceNumber, + arg.WorkspaceID, ) var i AIBridgeInterception err := row.Scan( @@ -1681,6 +1685,7 @@ func (q *sqlQuerier) InsertAIBridgeInterception(ctx context.Context, arg InsertA &i.AgentFirewallSequenceNumber, &i.ErrorType, &i.ErrorMessage, + &i.WorkspaceID, ) return i, err } @@ -2190,7 +2195,7 @@ WITH paginated_threads AS ( ) SELECT COALESCE(aibridge_interceptions.thread_root_id, aibridge_interceptions.id) AS thread_id, - aibridge_interceptions.id, aibridge_interceptions.initiator_id, aibridge_interceptions.provider, aibridge_interceptions.model, aibridge_interceptions.started_at, aibridge_interceptions.metadata, aibridge_interceptions.ended_at, aibridge_interceptions.api_key_id, aibridge_interceptions.client, aibridge_interceptions.thread_parent_id, aibridge_interceptions.thread_root_id, aibridge_interceptions.client_session_id, aibridge_interceptions.session_id, aibridge_interceptions.provider_name, aibridge_interceptions.credential_kind, aibridge_interceptions.credential_hint, aibridge_interceptions.agent_firewall_session_id, aibridge_interceptions.agent_firewall_sequence_number, aibridge_interceptions.error_type, aibridge_interceptions.error_message + aibridge_interceptions.id, aibridge_interceptions.initiator_id, aibridge_interceptions.provider, aibridge_interceptions.model, aibridge_interceptions.started_at, aibridge_interceptions.metadata, aibridge_interceptions.ended_at, aibridge_interceptions.api_key_id, aibridge_interceptions.client, aibridge_interceptions.thread_parent_id, aibridge_interceptions.thread_root_id, aibridge_interceptions.client_session_id, aibridge_interceptions.session_id, aibridge_interceptions.provider_name, aibridge_interceptions.credential_kind, aibridge_interceptions.credential_hint, aibridge_interceptions.agent_firewall_session_id, aibridge_interceptions.agent_firewall_sequence_number, aibridge_interceptions.error_type, aibridge_interceptions.error_message, aibridge_interceptions.workspace_id FROM aibridge_interceptions JOIN @@ -2258,6 +2263,7 @@ func (q *sqlQuerier) ListAIBridgeSessionThreads(ctx context.Context, arg ListAIB &i.AIBridgeInterception.AgentFirewallSequenceNumber, &i.AIBridgeInterception.ErrorType, &i.AIBridgeInterception.ErrorMessage, + &i.AIBridgeInterception.WorkspaceID, ); err != nil { return nil, err } @@ -2734,7 +2740,7 @@ UPDATE aibridge_interceptions WHERE id = $5::uuid AND ended_at IS NULL -RETURNING id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client, thread_parent_id, thread_root_id, client_session_id, session_id, provider_name, credential_kind, credential_hint, agent_firewall_session_id, agent_firewall_sequence_number, error_type, error_message +RETURNING id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client, thread_parent_id, thread_root_id, client_session_id, session_id, provider_name, credential_kind, credential_hint, agent_firewall_session_id, agent_firewall_sequence_number, error_type, error_message, workspace_id ` type UpdateAIBridgeInterceptionEndedParams struct { @@ -2775,6 +2781,7 @@ func (q *sqlQuerier) UpdateAIBridgeInterceptionEnded(ctx context.Context, arg Up &i.AgentFirewallSequenceNumber, &i.ErrorType, &i.ErrorMessage, + &i.WorkspaceID, ) return i, err } diff --git a/coderd/database/queries/aibridge.sql b/coderd/database/queries/aibridge.sql index 546fef5d84d..093fb03997b 100644 --- a/coderd/database/queries/aibridge.sql +++ b/coderd/database/queries/aibridge.sql @@ -1,8 +1,8 @@ -- name: InsertAIBridgeInterception :one INSERT INTO aibridge_interceptions ( - id, api_key_id, initiator_id, provider, provider_name, model, metadata, started_at, client, client_session_id, thread_parent_id, thread_root_id, credential_kind, credential_hint, agent_firewall_session_id, agent_firewall_sequence_number + id, api_key_id, initiator_id, provider, provider_name, model, metadata, started_at, client, client_session_id, thread_parent_id, thread_root_id, credential_kind, credential_hint, agent_firewall_session_id, agent_firewall_sequence_number, workspace_id ) VALUES ( - @id, @api_key_id, @initiator_id, @provider, @provider_name, @model, COALESCE(@metadata::jsonb, '{}'::jsonb), @started_at, @client, sqlc.narg('client_session_id'), sqlc.narg('thread_parent_interception_id')::uuid, sqlc.narg('thread_root_interception_id')::uuid, @credential_kind, @credential_hint, sqlc.narg('agent_firewall_session_id')::uuid, sqlc.narg('agent_firewall_sequence_number') + @id, @api_key_id, @initiator_id, @provider, @provider_name, @model, COALESCE(@metadata::jsonb, '{}'::jsonb), @started_at, @client, sqlc.narg('client_session_id'), sqlc.narg('thread_parent_interception_id')::uuid, sqlc.narg('thread_root_interception_id')::uuid, @credential_kind, @credential_hint, sqlc.narg('agent_firewall_session_id')::uuid, sqlc.narg('agent_firewall_sequence_number'), sqlc.narg('workspace_id')::uuid ) RETURNING *; diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 74688b62823..3846562a19e 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -3137,6 +3137,28 @@ func WorkspaceSessionTokenName(ownerID, workspaceID uuid.UUID) string { return fmt.Sprintf("%s_%s_session_token", ownerID, workspaceID) } +func ParseWorkspaceSessionTokenName(token string) (ownerID, workspaceID uuid.UUID, ok bool) { + prefix, ok := strings.CutSuffix(token, "_session_token") + if !ok { + return uuid.Nil, uuid.Nil, false + } + parts := strings.Split(prefix, "_") + if len(parts) != 2 { + return uuid.Nil, uuid.Nil, false + } + if id, err := uuid.Parse(parts[0]); err != nil { + return uuid.Nil, uuid.Nil, false + } else { // nolint:revive // author preference + ownerID = id + } + if id, err := uuid.Parse(parts[1]); err != nil { + return uuid.Nil, uuid.Nil, false + } else { // nolint:revive // author preference + workspaceID = id + } + return ownerID, workspaceID, true +} + func (s *server) regenerateSessionToken(ctx context.Context, user database.User, workspace database.Workspace) (string, error) { // NOTE(Cian): Once a workspace is claimed, there's no reason for the session token to be valid any longer. // Not generating any session token at all for a system user may unintentionally break existing templates, diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index ee7c125490e..f3fcf82e626 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -5,6 +5,7 @@ import ( crand "crypto/rand" "database/sql" "encoding/json" + "fmt" "io" "net/url" "slices" @@ -5316,3 +5317,61 @@ func TestDownloadFile(t *testing.T) { require.Equal(t, moduleData, data) }) } + +func TestWorkspaceSessionTokenName(t *testing.T) { + t.Parallel() + + ownerID, workspaceID := uuid.New(), uuid.New() + + for _, tc := range []struct { + name string + tokenName string + expectOwnerID string + expectWorkspaceID string + expectOK bool + }{ + { + name: "valid", + tokenName: provisionerdserver.WorkspaceSessionTokenName(ownerID, workspaceID), + expectOwnerID: ownerID.String(), + expectWorkspaceID: workspaceID.String(), + expectOK: true, + }, + { + name: "missing suffix", + tokenName: fmt.Sprintf("%s_%s", ownerID, workspaceID), + expectOwnerID: uuid.Nil.String(), + expectWorkspaceID: uuid.Nil.String(), + expectOK: false, + }, + { + name: "only one uuid", + tokenName: fmt.Sprintf("%s_session_token", ownerID), + expectOwnerID: uuid.Nil.String(), + expectWorkspaceID: uuid.Nil.String(), + expectOK: false, + }, + { + name: "invalid", + tokenName: "invalid_invalid_session_token", + expectOwnerID: uuid.Nil.String(), + expectWorkspaceID: uuid.Nil.String(), + expectOK: false, + }, + { + name: "empty", + tokenName: "", + expectOwnerID: uuid.Nil.String(), + expectWorkspaceID: uuid.Nil.String(), + expectOK: false, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ownerID, workspaceID, ok := provisionerdserver.ParseWorkspaceSessionTokenName(tc.tokenName) + require.Equal(t, tc.expectOwnerID, ownerID.String()) + require.Equal(t, tc.expectWorkspaceID, workspaceID.String()) + require.Equal(t, tc.expectOK, ok) + }) + } +} diff --git a/coderd/x/chatd/model_routing_aibridge.go b/coderd/x/chatd/model_routing_aibridge.go index 0e9abd561eb..07d570de604 100644 --- a/coderd/x/chatd/model_routing_aibridge.go +++ b/coderd/x/chatd/model_routing_aibridge.go @@ -70,11 +70,12 @@ const ( type aiGatewayRoundTripper struct { base http.RoundTripper apiKeyID string + attribution aibridge.Attribution providerAuth aiGatewayProviderAuth } func (t *aiGatewayRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - ctx := aibridge.WithDelegatedAPIKeyID(req.Context(), t.apiKeyID) + ctx := aibridge.WithDelegatedAttribution(aibridge.WithDelegatedAPIKeyID(req.Context(), t.apiKeyID), t.attribution) cloned := req.Clone(ctx) for name, value := range t.providerAuth.Headers { cloned.Header.Set(name, value) @@ -144,6 +145,12 @@ func (p *Server) newModel( ) } + // Use the trusted workspace binding already persisted on the chat. + attr := aibridge.Attribution{} + if req.Chat.WorkspaceID.Valid { + attr.WorkspaceID = req.Chat.WorkspaceID.UUID + } + factoryPtr := p.aibridgeTransportFactory if factoryPtr == nil { return chatprovider.Model{}, xerrors.New("AI Gateway transport factory is not configured") @@ -159,6 +166,7 @@ func (p *Server) newModel( baseRT := http.RoundTripper(&aiGatewayRoundTripper{ base: rt, apiKeyID: opts.ActiveAPIKeyID, + attribution: attr, providerAuth: route.ProviderAuth, }) if opts.RecordHTTP { diff --git a/coderd/x/chatd/model_routing_internal_test.go b/coderd/x/chatd/model_routing_internal_test.go index 5aa6ddb2ab5..8e16efa9c22 100644 --- a/coderd/x/chatd/model_routing_internal_test.go +++ b/coderd/x/chatd/model_routing_internal_test.go @@ -1040,3 +1040,144 @@ func TestAIBridgeDelegatedContextPropagation(t *testing.T) { require.Equal(t, "/v1/responses", got.path) require.Equal(t, apiKeyID, got.apiKeyID) } + +const openaiResponseBody = `{"id":"resp_test","object":"response","created_at":0,"status":"completed","model":"gpt-4","output":[{"id":"msg_test","type":"message","role":"assistant","content":[{"type":"output_text","text":"hello"}]}],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}` + +func openaiRoundTrip(t *testing.T, fn func(*http.Request)) roundTripFunc { + t.Helper() + return roundTripFunc(func(req *http.Request) (*http.Response, error) { + if fn != nil { + fn(req) + } + return &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(strings.NewReader(openaiResponseBody)), + Request: req, + }, nil + }) +} + +// TestNewModelAttribution verifies that newModel resolves Attribution from the +// chat's persisted WorkspaceID without any DB lookup and stamps it on every +// RoundTrip context alongside the delegated API key ID. +func TestNewModelAttribution(t *testing.T) { + t.Parallel() + + // nolint:gosec // Test-only identifier, not a credential. + const apiKeyID = "test-api-key-id" + provider := aibridgeTestAIProvider(uuid.New(), "primary-openai", database.AIProviderTypeOpenai) + + t.Run("NoWorkspace", func(t *testing.T) { + // When the chat has no workspace bound, Attribution has a zero WorkspaceID. + t.Parallel() + + chat := database.Chat{ + ID: uuid.New(), + OwnerID: uuid.New(), + // WorkspaceID is zero (not valid): no workspace bound. + } + + seen := make(chan aibridge.Attribution, 1) + factory := &aibridgeTestFactory{rt: openaiRoundTrip(t, func(req *http.Request) { + attr, _ := aibridge.DelegatedAttributionFromContext(req.Context()) + seen <- attr + })} + server := &Server{aibridgeTransportFactory: aibridgeTestFactoryPointer(factory)} + + model, err := server.newModel(t.Context(), aibridgeTestRequest(chat, "gpt-4"), aibridgeTestRoute(provider), modelBuildOptions{ActiveAPIKeyID: apiKeyID}) + require.NoError(t, err) + _, err = model.LanguageModel().Generate(t.Context(), fantasy.Call{Prompt: []fantasy.Message{{ + Role: fantasy.MessageRoleUser, + Content: []fantasy.MessagePart{fantasy.TextPart{Text: "hello"}}, + }}}) + require.NoError(t, err) + + got := <-seen + require.Equal(t, uuid.Nil, got.WorkspaceID) + }) + + t.Run("WithWorkspace", func(t *testing.T) { + // When the chat has a persisted WorkspaceID, Attribution carries it + // directly without any DB lookup. + t.Parallel() + + wsID := uuid.New() + chat := database.Chat{ + ID: uuid.New(), + OwnerID: uuid.New(), + WorkspaceID: uuid.NullUUID{UUID: wsID, Valid: true}, + } + + seen := make(chan aibridge.Attribution, 1) + factory := &aibridgeTestFactory{rt: openaiRoundTrip(t, func(req *http.Request) { + attr, _ := aibridge.DelegatedAttributionFromContext(req.Context()) + seen <- attr + })} + // No db mock: attribution must not require a DB lookup. + server := &Server{aibridgeTransportFactory: aibridgeTestFactoryPointer(factory)} + + model, err := server.newModel(t.Context(), aibridgeTestRequest(chat, "gpt-4"), aibridgeTestRoute(provider), modelBuildOptions{ActiveAPIKeyID: apiKeyID}) + require.NoError(t, err) + _, err = model.LanguageModel().Generate(t.Context(), fantasy.Call{Prompt: []fantasy.Message{{ + Role: fantasy.MessageRoleUser, + Content: []fantasy.MessagePart{fantasy.TextPart{Text: "hello"}}, + }}}) + require.NoError(t, err) + + got := <-seen + require.Equal(t, wsID, got.WorkspaceID) + }) +} + +// TestNewModelAttributionImmutablePerRequest verifies that a chat retaining the +// same synthetic API key receives attribution from its current workspace +// binding. Detaching and reattaching must not retain an earlier workspace ID. +func TestNewModelAttributionImmutablePerRequest(t *testing.T) { + t.Parallel() + + const sharedAPIKeyID = "shared-synthetic-key" + provider := aibridgeTestAIProvider(uuid.New(), "primary-openai", database.AIProviderTypeOpenai) + wsID1 := uuid.New() + wsID2 := uuid.New() + chat := database.Chat{ID: uuid.New(), OwnerID: uuid.New()} + workspaceBindings := []uuid.NullUUID{ + {UUID: wsID1, Valid: true}, + {}, + {UUID: wsID2, Valid: true}, + } + + type seenRequest struct { + apiKeyID string + workspaceID uuid.UUID + } + seen := make(chan seenRequest, len(workspaceBindings)) + factory := &aibridgeTestFactory{rt: openaiRoundTrip(t, func(req *http.Request) { + apiKeyID, _ := aibridge.DelegatedAPIKeyIDFromContext(req.Context()) + attr, _ := aibridge.DelegatedAttributionFromContext(req.Context()) + seen <- seenRequest{apiKeyID: apiKeyID, workspaceID: attr.WorkspaceID} + })} + server := &Server{aibridgeTransportFactory: aibridgeTestFactoryPointer(factory)} + call := fantasy.Call{Prompt: []fantasy.Message{{ + Role: fantasy.MessageRoleUser, + Content: []fantasy.MessagePart{fantasy.TextPart{Text: "hello"}}, + }}} + + for _, workspaceID := range workspaceBindings { + chat.WorkspaceID = workspaceID + model, err := server.newModel(t.Context(), aibridgeTestRequest(chat, "gpt-4"), aibridgeTestRoute(provider), modelBuildOptions{ActiveAPIKeyID: sharedAPIKeyID}) + require.NoError(t, err) + _, err = model.LanguageModel().Generate(t.Context(), call) + require.NoError(t, err) + } + + got := make([]seenRequest, 0, len(workspaceBindings)) + for range workspaceBindings { + got = append(got, <-seen) + } + require.Equal(t, []seenRequest{ + {apiKeyID: sharedAPIKeyID, workspaceID: wsID1}, + {apiKeyID: sharedAPIKeyID, workspaceID: uuid.Nil}, + {apiKeyID: sharedAPIKeyID, workspaceID: wsID2}, + }, got) +} diff --git a/codersdk/aibridge.go b/codersdk/aibridge.go index 78a5f931554..894bb8f8dd6 100644 --- a/codersdk/aibridge.go +++ b/codersdk/aibridge.go @@ -2,6 +2,7 @@ package codersdk import ( "context" + "encoding/json" "fmt" "io" "net/http" @@ -226,6 +227,18 @@ type AIBridgeSessionThreadsTokenUsage struct { Metadata map[string]any `json:"metadata"` } +// AIBridgeAttribution contains the attribution fields recorded for one +// interception. +type AIBridgeAttribution map[string]string + +// MarshalJSON encodes unknown attribution as an empty object. +func (a AIBridgeAttribution) MarshalJSON() ([]byte, error) { + if a == nil { + return []byte("{}"), nil + } + return json.Marshal(map[string]string(a)) +} + // AIBridgeThread represents a single thread within a session. // A thread groups interceptions by their thread_root_id. type AIBridgeThread struct { @@ -238,7 +251,10 @@ type AIBridgeThread struct { StartedAt time.Time `json:"started_at" format:"date-time"` EndedAt *time.Time `json:"ended_at,omitempty" format:"date-time"` TokenUsage AIBridgeSessionThreadsTokenUsage `json:"token_usage"` - AgenticActions []AIBridgeAgenticAction `json:"agentic_actions"` + // Attribution contains attribution data from the root interception. + // Unknown attribution is serialized as an empty object. + Attribution AIBridgeAttribution `json:"attribution"` + AgenticActions []AIBridgeAgenticAction `json:"agentic_actions"` // ErrorType is the categorized terminal upstream error from the root // interception, or nil when the interception succeeded. See the // aibridge_interception_error_type enum for possible values. @@ -257,13 +273,18 @@ type AIBridgeThread struct { AgentFirewallSequenceNumber *int32 `json:"agent_firewall_sequence_number,omitempty"` } -// AIBridgeAgenticAction represents a tool call with associated -// thinking blocks and token usage from one or more interceptions. +// AIBridgeAgenticAction represents data from one interception, including +// tool calls, thinking blocks, and token usage. Tool-less child interceptions +// are represented as actions with an empty ToolCalls slice. type AIBridgeAgenticAction struct { - Model string `json:"model"` - TokenUsage AIBridgeSessionThreadsTokenUsage `json:"token_usage"` - Thinking []AIBridgeModelThought `json:"thinking"` - ToolCalls []AIBridgeToolCall `json:"tool_calls"` + InterceptionID uuid.UUID `json:"interception_id" format:"uuid"` + Model string `json:"model"` + // Attribution contains attribution data from this interception. + // Unknown attribution is serialized as an empty object. + Attribution AIBridgeAttribution `json:"attribution"` + TokenUsage AIBridgeSessionThreadsTokenUsage `json:"token_usage"` + Thinking []AIBridgeModelThought `json:"thinking"` + ToolCalls []AIBridgeToolCall `json:"tool_calls"` } // AIBridgeModelThought represents a single thinking block from diff --git a/codersdk/aibridge_test.go b/codersdk/aibridge_test.go new file mode 100644 index 00000000000..dffae7002db --- /dev/null +++ b/codersdk/aibridge_test.go @@ -0,0 +1,42 @@ +package codersdk_test + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/codersdk" +) + +func TestAIBridgeAttributionMarshalJSON(t *testing.T) { + t.Parallel() + + for _, tt := range []struct { + name string + attribution codersdk.AIBridgeAttribution + want string + }{ + {name: "nil", want: `{}`}, + {name: "empty", attribution: codersdk.AIBridgeAttribution{}, want: `{}`}, + {name: "known", attribution: codersdk.AIBridgeAttribution{"workspace_id": "workspace-id"}, want: `{"workspace_id":"workspace-id"}`}, + } { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + data, err := json.Marshal(tt.attribution) + require.NoError(t, err) + require.JSONEq(t, tt.want, string(data)) + + for _, value := range []any{ + codersdk.AIBridgeThread{Attribution: tt.attribution}, + codersdk.AIBridgeAgenticAction{Attribution: tt.attribution}, + } { + data, err := json.Marshal(value) + require.NoError(t, err) + var fields map[string]json.RawMessage + require.NoError(t, json.Unmarshal(data, &fields)) + require.JSONEq(t, tt.want, string(fields["attribution"])) + } + }) + } +} diff --git a/docs/reference/api/aigateway.md b/docs/reference/api/aigateway.md index e16dd415bed..94cfa6040e2 100644 --- a/docs/reference/api/aigateway.md +++ b/docs/reference/api/aigateway.md @@ -291,6 +291,11 @@ Alias: also available at /api/v2/aibridge/sessions/{session_id} for backward com "agent_firewall_session_id": "3735294f-18b1-4e7a-a269-99c30f0b30e7", "agentic_actions": [ { + "attribution": { + "property1": "string", + "property2": "string" + }, + "interception_id": "34d9b688-63ad-46f4-88b5-665c1e7f7824", "model": "string", "thinking": [ { @@ -325,6 +330,10 @@ Alias: also available at /api/v2/aibridge/sessions/{session_id} for backward com ] } ], + "attribution": { + "property1": "string", + "property2": "string" + }, "credential_hint": "string", "credential_kind": "string", "ended_at": "2019-08-24T14:15:22Z", diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 0a8a5e2ddd8..0b42e831400 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -320,6 +320,11 @@ title: Schemas ```json { + "attribution": { + "property1": "string", + "property2": "string" + }, + "interception_id": "34d9b688-63ad-46f4-88b5-665c1e7f7824", "model": "string", "thinking": [ { @@ -357,12 +362,29 @@ title: Schemas ### Properties -| Name | Type | Required | Restrictions | Description | -|---------------|----------------------------------------------------------------------------------------|----------|--------------|-------------| -| `model` | string | false | | | -| `thinking` | array of [codersdk.AIBridgeModelThought](#codersdkaibridgemodelthought) | false | | | -| `token_usage` | [codersdk.AIBridgeSessionThreadsTokenUsage](#codersdkaibridgesessionthreadstokenusage) | false | | | -| `tool_calls` | array of [codersdk.AIBridgeToolCall](#codersdkaibridgetoolcall) | false | | | +| Name | Type | Required | Restrictions | Description | +|-------------------|----------------------------------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------------------------------------------------------| +| `attribution` | [codersdk.AIBridgeAttribution](#codersdkaibridgeattribution) | false | | Attribution contains attribution data from this interception. Unknown attribution is serialized as an empty object. | +| `interception_id` | string | false | | | +| `model` | string | false | | | +| `thinking` | array of [codersdk.AIBridgeModelThought](#codersdkaibridgemodelthought) | false | | | +| `token_usage` | [codersdk.AIBridgeSessionThreadsTokenUsage](#codersdkaibridgesessionthreadstokenusage) | false | | | +| `tool_calls` | array of [codersdk.AIBridgeToolCall](#codersdkaibridgetoolcall) | false | | | + +## codersdk.AIBridgeAttribution + +```json +{ + "property1": "string", + "property2": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|------------------|--------|----------|--------------|-------------| +| `[any property]` | string | false | | | ## codersdk.AIBridgeConfig @@ -682,6 +704,11 @@ title: Schemas "agent_firewall_session_id": "3735294f-18b1-4e7a-a269-99c30f0b30e7", "agentic_actions": [ { + "attribution": { + "property1": "string", + "property2": "string" + }, + "interception_id": "34d9b688-63ad-46f4-88b5-665c1e7f7824", "model": "string", "thinking": [ { @@ -716,6 +743,10 @@ title: Schemas ] } ], + "attribution": { + "property1": "string", + "property2": "string" + }, "credential_hint": "string", "credential_kind": "string", "ended_at": "2019-08-24T14:15:22Z", @@ -827,6 +858,11 @@ title: Schemas "agent_firewall_session_id": "3735294f-18b1-4e7a-a269-99c30f0b30e7", "agentic_actions": [ { + "attribution": { + "property1": "string", + "property2": "string" + }, + "interception_id": "34d9b688-63ad-46f4-88b5-665c1e7f7824", "model": "string", "thinking": [ { @@ -861,6 +897,10 @@ title: Schemas ] } ], + "attribution": { + "property1": "string", + "property2": "string" + }, "credential_hint": "string", "credential_kind": "string", "ended_at": "2019-08-24T14:15:22Z", @@ -891,6 +931,7 @@ title: Schemas | `agent_firewall_sequence_number` | integer | false | | Agent firewall sequence number is the firewall sequence number from the root interception. Used to determine the position of this LLM request in the firewall event stream. Nil when the request did not pass through the agent firewall. | | `agent_firewall_session_id` | string | false | | Agent firewall session ID links this thread to an agent firewall confinement session. Nil when the request did not pass through the agent firewall. | | `agentic_actions` | array of [codersdk.AIBridgeAgenticAction](#codersdkaibridgeagenticaction) | false | | | +| `attribution` | [codersdk.AIBridgeAttribution](#codersdkaibridgeattribution) | false | | Attribution contains attribution data from the root interception. Unknown attribution is serialized as an empty object. | | `credential_hint` | string | false | | | | `credential_kind` | string | false | | | | `ended_at` | string | false | | | diff --git a/enterprise/coderd/aibridge_test.go b/enterprise/coderd/aibridge_test.go index 8fcc84830b3..a7084ea62c4 100644 --- a/enterprise/coderd/aibridge_test.go +++ b/enterprise/coderd/aibridge_test.go @@ -2183,6 +2183,8 @@ func TestAIBridgeGetSessionThreads(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) now := dbtime.Now() + rootWorkspaceID := uuid.New() + childWorkspaceID := uuid.New() // Create a session with one thread. Root interception + child // interception sharing thread_root_id. @@ -2193,6 +2195,7 @@ func TestAIBridgeGetSessionThreads(t *testing.T) { Model: "claude-4", StartedAt: now, ClientSessionID: sql.NullString{String: "thread-session", Valid: true}, + WorkspaceID: uuid.NullUUID{UUID: rootWorkspaceID, Valid: true}, }, &rootEndedAt) childEndedAt := now.Add(2 * time.Minute) @@ -2204,8 +2207,20 @@ func TestAIBridgeGetSessionThreads(t *testing.T) { ClientSessionID: sql.NullString{String: "thread-session", Valid: true}, ThreadRootInterceptionID: uuid.NullUUID{UUID: root.ID, Valid: true}, ThreadParentInterceptionID: uuid.NullUUID{UUID: root.ID, Valid: true}, + WorkspaceID: uuid.NullUUID{UUID: childWorkspaceID, Valid: true}, }, &childEndedAt) + unknownChildEndedAt := now.Add(3 * time.Minute) + unknownChild := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{ + InitiatorID: firstUser.UserID, + Provider: "anthropic", + Model: "claude-4", + StartedAt: now.Add(2 * time.Minute), + ClientSessionID: sql.NullString{String: "thread-session", Valid: true}, + ThreadRootInterceptionID: uuid.NullUUID{UUID: root.ID, Valid: true}, + ThreadParentInterceptionID: uuid.NullUUID{UUID: child.ID, Valid: true}, + }, &unknownChildEndedAt) + // Add a user prompt on the root. dbgen.AIBridgeUserPrompt(t, db, database.InsertAIBridgeUserPromptParams{ InterceptionID: root.ID, @@ -2260,15 +2275,6 @@ func TestAIBridgeGetSessionThreads(t *testing.T) { CreatedAt: now.Add(time.Minute), }) - // Add another tool usage on child. - dbgen.AIBridgeToolUsage(t, db, database.InsertAIBridgeToolUsageParams{ - InterceptionID: child.ID, - ProviderResponseID: "resp-2", - Tool: "write_file", - Input: `{"path": "/login.go"}`, - CreatedAt: now.Add(time.Minute + time.Second), - }) - res, err := client.AIBridgeGetSessionThreads(ctx, "thread-session", uuid.Nil, uuid.Nil, 0) require.NoError(t, err) require.Equal(t, "thread-session", res.ID) @@ -2278,7 +2284,7 @@ func TestAIBridgeGetSessionThreads(t *testing.T) { require.NotNil(t, res.PageStartedAt) require.NotNil(t, res.PageEndedAt) require.True(t, res.PageStartedAt.Equal(now), "PageStartedAt should equal root started_at") - require.True(t, res.PageEndedAt.Equal(childEndedAt), "PageEndedAt should equal child ended_at") + require.True(t, res.PageEndedAt.Equal(unknownChildEndedAt), "PageEndedAt should equal last child ended_at") thread := res.Threads[0] require.Equal(t, root.ID, thread.ID) @@ -2286,6 +2292,10 @@ func TestAIBridgeGetSessionThreads(t *testing.T) { require.Equal(t, "implement login feature", *thread.Prompt) require.Equal(t, "claude-4", thread.Model) require.Equal(t, "anthropic", thread.Provider) + rootAttribution := codersdk.AIBridgeAttribution{ + "workspace_id": rootWorkspaceID.String(), + } + require.Equal(t, rootAttribution, thread.Attribution) // Thread-level token aggregation require.EqualValues(t, 300, thread.TokenUsage.InputTokens) @@ -2296,10 +2306,16 @@ func TestAIBridgeGetSessionThreads(t *testing.T) { require.EqualValues(t, int64(50), thread.TokenUsage.Metadata["cache_read_input"]) require.EqualValues(t, int64(10), thread.TokenUsage.Metadata["cache_creation_input"]) - // Two agentic actions (one per interception with tool calls). - require.Len(t, thread.AgenticActions, 2) + // One action is emitted for every interception. Child actions remain + // present even without tool calls. + require.Len(t, thread.AgenticActions, 3) action1 := thread.AgenticActions[0] + require.Equal(t, root.ID, action1.InterceptionID) + rootActionAttribution := codersdk.AIBridgeAttribution{ + "workspace_id": rootWorkspaceID.String(), + } + require.Equal(t, rootActionAttribution, action1.Attribution) // Root interception has two tool calls. require.Len(t, action1.ToolCalls, 2) require.Equal(t, "read_file", action1.ToolCalls[0].Tool) @@ -2311,10 +2327,33 @@ func TestAIBridgeGetSessionThreads(t *testing.T) { require.EqualValues(t, 50, action1.TokenUsage.OutputTokens) action2 := thread.AgenticActions[1] - require.Len(t, action2.ToolCalls, 1) - require.Equal(t, "write_file", action2.ToolCalls[0].Tool) + require.Equal(t, child.ID, action2.InterceptionID) + childAttribution := codersdk.AIBridgeAttribution{ + "workspace_id": childWorkspaceID.String(), + } + require.Equal(t, childAttribution, action2.Attribution) + require.Empty(t, action2.ToolCalls) require.Empty(t, action2.Thinking) + action3 := thread.AgenticActions[2] + require.Equal(t, unknownChild.ID, action3.InterceptionID) + require.NotNil(t, action3.Attribution) + require.Empty(t, action3.Attribution) + require.Empty(t, action3.ToolCalls) + require.Empty(t, action3.Thinking) + + var raw struct { + Threads []struct { + AgenticActions []struct { + Attribution json.RawMessage `json:"attribution"` + } `json:"agentic_actions"` + } `json:"threads"` + } + jsonBytes, err := json.Marshal(res) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(jsonBytes, &raw)) + require.JSONEq(t, `{}`, string(raw.Threads[0].AgenticActions[2].Attribution)) + // Session-level token aggregation. require.EqualValues(t, 300, res.TokenUsageSummary.InputTokens) require.EqualValues(t, 150, res.TokenUsageSummary.OutputTokens) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 7408a0c1e1e..43eeea56d04 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -12,16 +12,30 @@ export interface ACLAvailable { // From codersdk/aibridge.go /** - * AIBridgeAgenticAction represents a tool call with associated - * thinking blocks and token usage from one or more interceptions. + * AIBridgeAgenticAction represents data from one interception, including + * tool calls, thinking blocks, and token usage. Tool-less child interceptions + * are represented as actions with an empty ToolCalls slice. */ export interface AIBridgeAgenticAction { + readonly interception_id: string; readonly model: string; + /** + * Attribution contains attribution data from this interception. + * Unknown attribution is serialized as an empty object. + */ + readonly attribution: AIBridgeAttribution; readonly token_usage: AIBridgeSessionThreadsTokenUsage; readonly thinking: readonly AIBridgeModelThought[]; readonly tool_calls: readonly AIBridgeToolCall[]; } +// From codersdk/aibridge.go +/** + * AIBridgeAttribution contains the attribution fields recorded for one + * interception. + */ +export type AIBridgeAttribution = Record; + // From codersdk/deployment.go export interface AIBridgeConfig { readonly enabled: boolean; @@ -227,6 +241,11 @@ export interface AIBridgeThread { readonly started_at: string; readonly ended_at?: string; readonly token_usage: AIBridgeSessionThreadsTokenUsage; + /** + * Attribution contains attribution data from the root interception. + * Unknown attribution is serialized as an empty object. + */ + readonly attribution: AIBridgeAttribution; readonly agentic_actions: readonly AIBridgeAgenticAction[]; /** * ErrorType is the categorized terminal upstream error from the root diff --git a/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/SessionTimeline.stories.tsx b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/SessionTimeline.stories.tsx index f32b4a88c2e..badeba3bbfe 100644 --- a/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/SessionTimeline.stories.tsx +++ b/site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/SessionTimeline.stories.tsx @@ -48,7 +48,9 @@ const mockThreadLong: AIBridgeThread = { }, agentic_actions: [ { + interception_id: "interception-2", model: "claude-opus-4-6", + attribution: { workspace_id: "workspace-2" }, token_usage: { input_tokens: 2800, output_tokens: 1100, @@ -69,6 +71,21 @@ const mockThreadLong: AIBridgeThread = { metadata: {}, created_at: "2026-03-09T10:00:15.000Z", }, + ], + }, + { + interception_id: "interception-3", + model: "claude-opus-4-6", + attribution: { workspace_id: "workspace-3" }, + token_usage: { + input_tokens: 2800, + output_tokens: 1100, + cache_read_input_tokens: 1800, + cache_write_input_tokens: 500, + metadata: {}, + }, + thinking: [], + tool_calls: [ { id: "tool-2b", interception_id: "interception-3", @@ -85,6 +102,20 @@ const mockThreadLong: AIBridgeThread = { }, ], }, + { + interception_id: "interception-4", + model: "claude-opus-4-6", + attribution: {}, + token_usage: { + input_tokens: 0, + output_tokens: 0, + cache_read_input_tokens: 0, + cache_write_input_tokens: 0, + metadata: {}, + }, + thinking: [], + tool_calls: [], + }, ], }; diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 8ed09b4ddc6..0e448a20b21 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -5423,9 +5423,16 @@ export const MockAIBridgeThread: TypesGen.AIBridgeThread = { cache_write_input_tokens: 140, metadata: {}, }, + attribution: { + workspace_id: "workspace-1", + }, agentic_actions: [ { + interception_id: "interception-1", model: "claude-opus-4-6", + attribution: { + workspace_id: "workspace-1", + }, token_usage: { input_tokens: 620, output_tokens: 160,