diff --git a/assets.go b/assets.go
new file mode 100644
index 0000000000000000000000000000000000000000..b3ed90c1011a144565e9fc8f293473352ba6cba3
--- /dev/null
+++ b/assets.go
@@ -0,0 +1,168 @@
+package perxis
+
+import (
+	"io"
+	"io/fs"
+	"path/filepath"
+
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	jsoniter "github.com/json-iterator/go"
+	"gopkg.in/yaml.v3"
+)
+
+// Assets предоставляет методы для загрузки данных из файловой системы
+type Assets[T any] struct {
+	Constructor func() T
+}
+
+// NewAssets возвращает новый экземпляр загрузчика
+func NewAssets[T any]() *Assets[T] {
+	return &Assets[T]{
+		Constructor: func() (t T) { return t },
+	}
+}
+
+type FromFSFunc[T any] func(fsys fs.FS) ([]T, error)
+type FromFileFunc[T any] func(file fs.File) ([]T, error)
+
+func (a *Assets[T]) Funcs() (FromFSFunc[T], FromFileFunc[T]) {
+	return a.FromFS, a.FromFile
+}
+
+// WithConstructor устанавливает конструктор для создания новых экземпляров
+func (a *Assets[T]) WithConstructor(t func() T) *Assets[T] {
+	a.Constructor = t
+	return a
+}
+
+// MustFrom возвращает все записи в переданной файловой системе
+func (a *Assets[T]) MustFrom(fsys fs.FS, path string) []T {
+	res, err := a.From(fsys, path)
+	if err != nil {
+		panic(err)
+	}
+	return res
+}
+
+// MustOneFrom возвращает одну запись из переданного файла
+func (a *Assets[T]) MustOneFrom(fsys fs.FS, path string) T {
+	res, err := a.From(fsys, path)
+	if err != nil {
+		panic(err)
+	}
+	if len(res) == 0 {
+		panic(errors.Errorf("no entries found"))
+	}
+	if len(res) > 1 {
+		panic(errors.Errorf("multiple entries found"))
+	}
+	return res[0]
+}
+
+// From возвращает записи из переданного файла
+func (a *Assets[T]) From(fsys fs.FS, path string) ([]T, error) {
+	f, err := fsys.Open(path)
+	if err != nil {
+		return nil, err
+	}
+
+	defer f.Close()
+
+	stat, err := f.Stat()
+	if err != nil {
+		return nil, err
+	}
+
+	if stat.IsDir() {
+		sub, err := fs.Sub(fsys, path)
+		if err != nil {
+			return nil, err
+		}
+		return a.FromFS(sub)
+	}
+
+	return a.FromFile(f)
+}
+
+// FromFile возвращает записи в переданном файле
+func (a *Assets[T]) FromFile(file fs.File) ([]T, error) {
+	stat, err := file.Stat()
+	if err != nil {
+		return nil, err
+	}
+
+	switch filepath.Ext(stat.Name()) {
+	case ".json":
+		entry, err := a.FromJSON(file)
+		if err != nil {
+			return nil, errors.Wrapf(err, "file '%s'", stat.Name())
+		}
+		return []T{entry}, nil
+
+	case ".yaml", ".yml":
+		entries, err := a.FromYAML(file)
+		return entries, errors.Wrapf(err, "file '%s'", stat.Name())
+	}
+
+	return nil, errors.Errorf("file '%s' must be in JSON or YAML format", stat.Name())
+}
+
+// FromFS возвращает все записи в переданной файловой системе
+func (a *Assets[T]) FromFS(fsys fs.FS) (result []T, err error) {
+	if err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, _ error) error {
+		file, err := fsys.Open(path)
+		if err != nil {
+			return err
+		}
+		defer file.Close()
+
+		if entries, err := a.FromFile(file); err == nil {
+			result = append(result, entries...)
+		}
+		return nil
+	}); err != nil {
+		return nil, err
+	}
+
+	return result, nil
+}
+
+// FromJSON возвращает запись из JSON
+func (c *Assets[T]) FromJSON(r io.Reader) (T, error) {
+	entry := c.Constructor()
+	data, err := io.ReadAll(r)
+	if err != nil {
+		return entry, err
+	}
+
+	err = jsoniter.Unmarshal(data, &entry)
+	return entry, err
+}
+
+// FromYAML возвращает записи из YAML
+func (c *Assets[T]) FromYAML(r io.Reader) (result []T, err error) {
+	decoder := yaml.NewDecoder(r)
+	for {
+		var data interface{}
+		err = decoder.Decode(&data)
+		if errors.Is(err, io.EOF) {
+			break
+		}
+		if err != nil {
+			return nil, err
+		}
+
+		json, err := jsoniter.Marshal(data)
+		if err != nil {
+			return nil, err
+		}
+
+		entry := c.Constructor()
+		if err = jsoniter.Unmarshal(json, &entry); err != nil {
+			return nil, err
+		}
+		result = append(result, entry)
+	}
+
+	return result, nil
+}
diff --git a/pkg/assets/test/assets/invalid.txt b/assets/tests/invalid.txt
similarity index 100%
rename from pkg/assets/test/assets/invalid.txt
rename to assets/tests/invalid.txt
diff --git a/pkg/assets/test/assets/item.json b/assets/tests/item.json
similarity index 100%
rename from pkg/assets/test/assets/item.json
rename to assets/tests/item.json
diff --git a/pkg/assets/test/assets/items.yaml b/assets/tests/items.yaml
similarity index 100%
rename from pkg/assets/test/assets/items.yaml
rename to assets/tests/items.yaml
diff --git a/assets_test.go b/assets_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..25f33bb5e7d5d20fd01f25ed248e661fb6c19bf0
--- /dev/null
+++ b/assets_test.go
@@ -0,0 +1,86 @@
+package perxis
+
+import (
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+type State int
+
+const (
+	State1 State = iota
+	State2
+)
+
+type testEntry struct {
+	ID     string
+	Enum   State
+	Data   map[string]interface{}
+	Struct *nested
+}
+
+type nested struct {
+	Option *bool
+}
+
+func TestFromFS(t *testing.T) {
+	tr := true
+	i1 := &testEntry{
+		ID:   "item1",
+		Enum: State2,
+		Data: map[string]interface{}{
+			"obj": map[string]interface{}{"str": "value"},
+			"arr": []interface{}{"str1", "str2"},
+		},
+		Struct: &nested{
+			Option: &tr,
+		},
+	}
+
+	i2 := *i1
+	i2.ID = "item2"
+
+	i3 := *i1
+	i3.ID = "item3"
+
+	assets := NewAssets[*testEntry]()
+	r, err := assets.FromFS(os.DirFS("assets/tests"))
+	require.NoError(t, err)
+	require.Len(t, r, 3)
+	assert.ElementsMatch(t, []*testEntry{i1, &i2, &i3}, r)
+}
+
+func TestFrom(t *testing.T) {
+	tr := true
+	i1 := &testEntry{
+		ID:   "item1",
+		Enum: State2,
+		Data: map[string]interface{}{
+			"obj": map[string]interface{}{"str": "value"},
+			"arr": []interface{}{"str1", "str2"},
+		},
+		Struct: &nested{
+			Option: &tr,
+		},
+	}
+
+	i2 := *i1
+	i2.ID = "item2"
+
+	i3 := *i1
+	i3.ID = "item3"
+
+	assets := NewAssets[*testEntry]()
+	r, err := assets.From(os.DirFS("assets"), "tests")
+	require.NoError(t, err)
+	require.Len(t, r, 3)
+	assert.ElementsMatch(t, []*testEntry{i1, &i2, &i3}, r)
+
+	r, err = assets.From(os.DirFS("assets"), "tests/items.yaml")
+	require.NoError(t, err)
+	require.Len(t, r, 2)
+	assert.Equal(t, []*testEntry{i1, &i2}, r)
+}
diff --git a/pkg/assets/config.go b/pkg/assets/config.go
deleted file mode 100644
index 3c2865571f619fd7f48a731aa984d2b8a24deabb..0000000000000000000000000000000000000000
--- a/pkg/assets/config.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package assets
-
-import "io/fs"
-
-// Для использования без объявления конфигурации:
-// package items
-// var FromFS = assets.FromFS[*Item]
-func FromFS[T any](fsys fs.FS) ([]T, error) {
-	return New[T]().FromFS(fsys)
-}
-
-type Config[T any] struct {
-	Constructor func() T
-}
-
-// Для использования с настройкой:
-// package schema
-// var FromFS = assets.New[*Schema]().WithConstructor(customFunc).FromFS
-func New[T any]() *Config[T] {
-	return &Config[T]{
-		Constructor: func() (t T) { return t },
-	}
-}
-
-func (c *Config[T]) WithConstructor(t func() T) *Config[T] {
-	c.Constructor = t
-	return c
-}
diff --git a/pkg/assets/files.go b/pkg/assets/files.go
deleted file mode 100644
index dd200628d3421d27404210499a081c08ff47359b..0000000000000000000000000000000000000000
--- a/pkg/assets/files.go
+++ /dev/null
@@ -1,50 +0,0 @@
-package assets
-
-import (
-	"io/fs"
-	"path/filepath"
-
-	"git.perx.ru/perxis/perxis-go/pkg/errors"
-)
-
-func (c *Config[T]) FromFile(file fs.File) ([]T, error) {
-	stat, err := file.Stat()
-	if err != nil {
-		return nil, err
-	}
-
-	switch filepath.Ext(stat.Name()) {
-	case ".json":
-		entry, err := c.FromJSON(file)
-		if err != nil {
-			return nil, errors.Wrapf(err, "file '%s'", stat.Name())
-		}
-		return []T{entry}, nil
-
-	case ".yaml", ".yml":
-		entries, err := c.FromYAML(file)
-		return entries, errors.Wrapf(err, "file '%s'", stat.Name())
-	}
-
-	return nil, errors.Errorf("file '%s' must be in JSON or YAML format", stat.Name())
-}
-
-// FromFS возвращает все валидные записи в переданной файловой системе
-func (c *Config[T]) FromFS(fsys fs.FS) (result []T, err error) {
-	if err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, _ error) error {
-		file, err := fsys.Open(path)
-		if err != nil {
-			return err
-		}
-		defer file.Close()
-
-		if entries, err := c.FromFile(file); err == nil {
-			result = append(result, entries...)
-		}
-		return nil
-	}); err != nil {
-		return nil, err
-	}
-
-	return result, nil
-}
diff --git a/pkg/assets/test/files_test.go b/pkg/assets/test/files_test.go
deleted file mode 100644
index 908cd3d282a98c2b9da39ec2b7a42fcf20673b24..0000000000000000000000000000000000000000
--- a/pkg/assets/test/files_test.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package test
-
-import (
-	"os"
-	"testing"
-
-	"git.perx.ru/perxis/perxis-go/pkg/assets"
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-)
-
-type State int
-
-const (
-	State1 State = iota
-	State2
-)
-
-type testEntry struct {
-	ID     string
-	Enum   State
-	Data   map[string]interface{}
-	Struct *nested
-}
-
-type nested struct {
-	Option *bool
-}
-
-func TestFromFS(t *testing.T) {
-	tr := true
-	i1 := &testEntry{
-		ID:   "item1",
-		Enum: State2,
-		Data: map[string]interface{}{
-			"obj": map[string]interface{}{"str": "value"},
-			"arr": []interface{}{"str1", "str2"},
-		},
-		Struct: &nested{
-			Option: &tr,
-		},
-	}
-
-	i2 := *i1
-	i2.ID = "item2"
-
-	i3 := *i1
-	i3.ID = "item3"
-
-	r, err := assets.FromFS[*testEntry](os.DirFS("assets"))
-	require.NoError(t, err)
-	require.Len(t, r, 3)
-	assert.ElementsMatch(t, []*testEntry{i1, &i2, &i3}, r)
-}
diff --git a/pkg/assets/umarshal.go b/pkg/assets/umarshal.go
deleted file mode 100644
index 53937ac6f3dcc5c18ab319c49f8e850667f342ac..0000000000000000000000000000000000000000
--- a/pkg/assets/umarshal.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package assets
-
-import (
-	"errors"
-	"io"
-
-	jsoniter "github.com/json-iterator/go"
-	"gopkg.in/yaml.v3"
-)
-
-func (c *Config[T]) FromJSON(r io.Reader) (T, error) {
-	entry := c.Constructor()
-	data, err := io.ReadAll(r)
-	if err != nil {
-		return entry, err
-	}
-
-	err = jsoniter.Unmarshal(data, &entry)
-	return entry, err
-}
-
-func (c *Config[T]) FromYAML(r io.Reader) (result []T, err error) {
-	decoder := yaml.NewDecoder(r)
-	for {
-		var data interface{}
-		err = decoder.Decode(&data)
-		if errors.Is(err, io.EOF) {
-			break
-		}
-		if err != nil {
-			return nil, err
-		}
-
-		json, err := jsoniter.Marshal(data)
-		if err != nil {
-			return nil, err
-		}
-
-		entry := c.Constructor()
-		if err = jsoniter.Unmarshal(json, &entry); err != nil {
-			return nil, err
-		}
-		result = append(result, entry)
-	}
-
-	return result, nil
-}
diff --git a/pkg/clients/client.go b/pkg/clients/client.go
index 86f9dc6c4951776b053b94966ecb7796a7688cd8..f38b5acc9b442be39139523bcd316947d5d54fe7 100644
--- a/pkg/clients/client.go
+++ b/pkg/clients/client.go
@@ -1,7 +1,5 @@
 package clients
 
-import "git.perx.ru/perxis/perxis-go/pkg/assets"
-
 // Client - приложение имеющее доступ к API
 type Client struct {
 	// Внутренний идентификатор клиента внутри системы
@@ -28,8 +26,6 @@ type Client struct {
 	RoleID string `json:"role_id" bson:"role_id"`
 }
 
-var FromFS = assets.FromFS[*Client]
-
 type OAuth struct {
 	ClientID     string `bson:"client_id,omitempty" json:"client_id,omitempty"`         // Идентификатор клиента выданные IdP сервером, используется для идентификации клиента
 	AuthID       string `bson:"auth_id,omitempty" json:"auth_id,omitempty"`             // Сервис, который используется для авторизации клиента
diff --git a/pkg/extension/extension.go b/pkg/extension/extension.go
index ae7878d56de53f423ea86c28fa2f263cef6cbd03..ee87e981e6d31bb39b52516870d3bc637182a5af 100644
--- a/pkg/extension/extension.go
+++ b/pkg/extension/extension.go
@@ -3,6 +3,7 @@ package extension
 import (
 	"context"
 
+	"git.perx.ru/perxis/perxis-go"
 	"git.perx.ru/perxis/perxis-go/pkg/collections"
 	"git.perx.ru/perxis/perxis-go/pkg/content"
 	"git.perx.ru/perxis/perxis-go/pkg/errors"
@@ -41,6 +42,9 @@ var (
 
 	ErrNotInstalled     = errors.New("not installed")
 	ErrUnknownExtension = errors.New("unknown extension")
+
+	ManifestAssets   = perxis.NewAssets[*ExtensionDescriptor]()
+	ManifestFromFile = ManifestAssets.MustOneFrom
 )
 
 // Runnable описывает интерфейс сервиса с запуском и остановкой. Вызывается сервером расширений
diff --git a/pkg/items/item.go b/pkg/items/item.go
index ade303296edc848ffcbb3d9e334e24ada6c9ccb4..1a226a7627a0435a2b77480ebc2095e126dae37d 100644
--- a/pkg/items/item.go
+++ b/pkg/items/item.go
@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"time"
 
-	"git.perx.ru/perxis/perxis-go/pkg/assets"
 	"git.perx.ru/perxis/perxis-go/pkg/data"
 	"git.perx.ru/perxis/perxis-go/pkg/errors"
 	"git.perx.ru/perxis/perxis-go/pkg/locales"
@@ -135,8 +134,6 @@ type Item struct {
 	Template bool `json:"template" bson:"template,omitempty"`
 }
 
-var FromFS = assets.FromFS[*Item]
-
 func NewItem(spaceID, envID, collID, id string, data map[string]interface{}, translations map[string]map[string]interface{}) *Item {
 	return &Item{
 		ID:           id,
diff --git a/pkg/roles/role.go b/pkg/roles/role.go
index 4e3cf063ec8c8536075829988172aba26dcbb96f..579a9664ca87382c7c7533ecf502df3d6cb5c990 100644
--- a/pkg/roles/role.go
+++ b/pkg/roles/role.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"slices"
 
-	"git.perx.ru/perxis/perxis-go/pkg/assets"
 	"git.perx.ru/perxis/perxis-go/pkg/data"
 	"git.perx.ru/perxis/perxis-go/pkg/environments"
 	"git.perx.ru/perxis/perxis-go/pkg/permission"
@@ -36,8 +35,6 @@ type Role struct {
 	AllowManagement bool `json:"allow_management" bson:"allow_management"`
 }
 
-var FromFS = assets.FromFS[*Role]
-
 func (r Role) Clone() *Role {
 	return &Role{
 		ID:              r.ID,
diff --git a/pkg/schema/schema.go b/pkg/schema/schema.go
index 0788c9eecf388662dcdd4592a5a6cd2205454a2f..b61c696316030afd5f71c9611b05d0c1b7e6863b 100644
--- a/pkg/schema/schema.go
+++ b/pkg/schema/schema.go
@@ -4,7 +4,7 @@ import (
 	"context"
 	"reflect"
 
-	"git.perx.ru/perxis/perxis-go/pkg/assets"
+	"git.perx.ru/perxis/perxis-go"
 	"git.perx.ru/perxis/perxis-go/pkg/errors"
 	"git.perx.ru/perxis/perxis-go/pkg/expr"
 	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
@@ -33,7 +33,9 @@ var (
 	Validate = validate.Validate
 	Evaluate = field.Evaluate
 
-	FromFS = assets.New[*Schema]().WithConstructor(func() *Schema { return New() }).FromFS
+	Assets   = perxis.NewAssets[*Schema]().WithConstructor(func() *Schema { return New() })
+	FromFS   = Assets.FromFS
+	FromFile = Assets.FromFile
 )
 
 func (s *Schema) Clone(reset bool) *Schema {
diff --git a/pkg/setup/config.go b/pkg/setup/config.go
index cb5368b012f89f555d156c43b433cce2f9cbccae..08e1c740323d4e709e0a2d905ed2b2af2d4ebc5b 100644
--- a/pkg/setup/config.go
+++ b/pkg/setup/config.go
@@ -4,6 +4,8 @@ import (
 	"errors"
 	"io/fs"
 
+	"git.perx.ru/perxis/perxis-go"
+
 	"git.perx.ru/perxis/perxis-go/pkg/clients"
 	"git.perx.ru/perxis/perxis-go/pkg/collections"
 	"git.perx.ru/perxis/perxis-go/pkg/items"
@@ -110,11 +112,12 @@ func (cfg *Config) WithClientsOptions(filter func(c *clients.Client) bool, opts
 
 // LoadItems загружает элементы из указанной файловой системы
 func (cfg *Config) LoadClients(fsys fs.FS, opt ...ClientsOption) (*Config, error) {
-	clients, err := clients.FromFS(fsys)
+	assets := perxis.NewAssets[*clients.Client]()
+	cls, err := assets.FromFS(fsys)
 	if err != nil {
 		return nil, err
 	}
-	return cfg.AddClients(clients, opt...), nil
+	return cfg.AddClients(cls, opt...), nil
 }
 
 func (cfg *Config) MustLoadClients(fsys fs.FS, opt ...ClientsOption) *Config {
@@ -141,11 +144,12 @@ func (c *Config) AddClient(client *clients.Client, opt ...ClientsOption) *Config
 
 // LoadItems загружает элементы из указанной файловой системы
 func (cfg *Config) LoadRoles(fsys fs.FS, opt ...RolesOption) (*Config, error) {
-	roles, err := roles.FromFS(fsys)
+	assets := perxis.NewAssets[*roles.Role]()
+	rls, err := assets.FromFS(fsys)
 	if err != nil {
 		return nil, err
 	}
-	return cfg.AddRoles(roles, opt...), nil
+	return cfg.AddRoles(rls, opt...), nil
 }
 
 func (cfg *Config) MustLoadRoles(fsys fs.FS, opt ...RolesOption) *Config {
@@ -249,7 +253,8 @@ func (cfg *Config) GetCollectionConfig(id string) *CollectionConfig {
 
 // LoadItems загружает элементы из указанной файловой системы
 func (cfg *Config) LoadItems(fsys fs.FS, opt ...ItemsOption) (*Config, error) {
-	itms, err := items.FromFS(fsys)
+	assets := perxis.NewAssets[*items.Item]()
+	itms, err := assets.FromFS(fsys)
 	if err != nil {
 		return nil, err
 	}