Skip to main content
Glama
Romandredan

odata1c-gate

by Romandredan
README.md
# odata1c-gate

Локальный MCP-шлюз между Claude Code и стандартным OData-интерфейсом 1С:Предприятие 8.3.
Модель получает доступ к данным базы — справочникам, документам, регистрам, — но между ней и 1С
стоит **гейт псевдонимизации**: реквизиты (ИНН, счета, паспорта, телефоны) и, на выбранном уровне,
названия организаций и ФИО заменяются токенами вида `[[type:tail]]` до того, как данные увидит
модель. Обратная подмена происходит только внутри шлюза, перед отправкой запроса в 1С.

Один пользователь, одна машина, несколько сессий агентов одновременно, несколько баз 1С.
Клиент — Claude Code (другие клиенты MCP работают иначе, см. раздел «Ограничения» ниже и
[docs/install.md](docs/install.md)).

## Зачем это

Дать модели читать рабочую базу 1С — значит отдать ей персональные данные и коммерческую тайну:
ИНН контрагентов, расчётные счета, телефоны и адреса физических лиц, названия клиентов.
Обезличивать выгрузку заранее не всегда удобно (модель должна видеть свежие данные и уметь
дозапрашивать их при необходимости), а инструктировать модель «не показывай ИНН» бессмысленно:
инструкция — не механизм, да и провайдер модели всё равно увидит даже то, что не было «показано»
пользователю.

Шлюз решает это подменой на границе: модель работает с живой базой, но защищаемые значения
заменяются токенами до того, как попадут в её контекст. Токен детерминирован (одно значение — один
токен), поэтому по нему можно отбирать, связывать записи и вести разговор, не зная исходного
значения. Разработчик, который читает ответы модели, видит `[[inn:M4T2Q9XZ7K]]`, а не ИНН — и при
необходимости раскрывает его сам, командой в терминале, мимо модели.

## Что видит модель, а что нет

**Что уходит модели.** Структура базы (сущности, поля, ключи, навигация) и данные, прошедшие
гейт. Номера и даты документов, суммы, количества, коды, GUID, значения перечислений не
защищаются ни на одном уровне — без них работа с базой теряет смысл. Названия организаций и ФИО
(классы `org` и `person`) и защищаемые реквизиты — ИНН, КПП, ОГРН, счета, БИК, карты, СНИЛС,
документы, телефоны, почта, даты рождения, адреса — приходят токенами `[[type:tail]]` по правилам
политики базы. Уровень задаётся на базу: `off` — гейт выключен, `identifiers` — реквизиты,
`identifiers+names` — реквизиты плюс названия и ФИО.

**Что не уходит никогда.** Реальные значения защищаемых классов не выходят через MCP ни в одном
ответе: ни в данных, ни в текстах ошибок 1С, ни в превью записи, ни в журнале, ни в
`odata1c_raw_get`, ни в вопросах подтверждения. Это инвариант, а не тест: последний проход по
готовому ответу делает страж утечек — он ищет в сериализованном ответе известные словарю значения
и заменяет их токенами, если что-то прошло мимо гейта. Учётные данные 1С модель не получает:
пароль не покидает домашнего каталога вовсе, имя пользователя 1С не попадает ни в один ответ, а
адрес публикации базы не возвращает ни один тул. О самой базе модель узнаёт ровно то, что
перечисляет `odata1c_bases`: имя, подпись, роль, уровень гейта, разрешена ли запись, состояние
индекса (собран ли, когда, сколько сущностей) и конфигурацию 1С, к которой база отнесена.
Единственное исключение по адресу — текст сетевого сбоя, в который его может вписать HTTP-клиент.
Хук плагина вдобавок запрещает модели читать файлы домашнего каталога шлюза. Раскрыть токен может
только владелец машины, командой `odata1c reveal` в своём терминале.

**Кто подтверждает запись.** Тулы записи ничего не пишут в 1С: они готовят операцию, показывают
превью в токенах и возвращают `pending_id`. Выполняет её отдельный вызов `odata1c_commit`, и
только после подтверждения человека механизмом клиента — в Claude Code это диалог разрешения,
у клиентов с elicitation — вопрос шлюза. Реплика «да» в чате подтверждением не считается: модель
не может подтвердить запись сама себе. Каждая выполненная запись попадает в локальный журнал и
откатывается тулом `odata1c_undo`. Записи в базах с ролью `prod` по умолчанию нет вовсе, а состав
разрешённого сужается флагами разрешений в настройках базы.

## Установка

Нужны [`uv`](https://docs.astral.sh/uv/) (сам поставит подходящий Python) и Claude Code. Дальше
три команды в терминале:

```text
claude plugin marketplace add Romandredan/odata1c-gate
claude plugin install odata1c@odata1c-gate
uv tool install odata1c-gate
```

Первые две ставят плагин Claude Code: MCP-сервер шлюза, четыре навыка, хук подтверждения записи и
агента-следователя. Версия пакета закреплена в `plugin/.mcp.json`, поэтому `claude plugin update
odata1c` обновляет и плагин, и шлюз.

Третья ставит отдельно командную строку — она нужна владельцу базы, а не модели (описать базу,
собрать индекс, раскрыть токен, править политику гейта). После неё команда `odata1c` доступна в
терминале; без установки то же самое запускается как `uvx --from odata1c-gate odata1c <команда>`.

Подробности, Linux, обновление и разбор типовых сбоев — [docs/install.md](docs/install.md).

## Сразу после установки

Откройте Claude Code и скажите: **«подключи базу 1С»**. Навык `odata1c-setup` спросит, как назвать
базу и какая у неё роль, и даст одну команду для вашего терминала. Команда запросит адрес
публикации, пользователя 1С и пароль. Пароль вводится только там, модель его не видит. После
этого Claude сам построит индекс метаданных, проверит, что база отвечает, и расскажет, что в ней
защищено.

То же самое можно сделать вручную:

```text
odata1c base add ut_test --role test --recipes ut   # спросит адрес, подпись, пользователя, пароль
odata1c reindex ut_test                             # построить индекс метаданных
```

Первая команда сама создаёт домашний каталог шлюза, отдельный `odata1c init` не нужен. Если что-то
не работает, `odata1c doctor` покажет, где именно: `uv`, настройки, базы, шлюз, Claude Code.

Адрес базы — это адрес публикации OData 1С, он оканчивается на `/odata/standard.odata/`
(например `https://server/base/odata/standard.odata/`). Пользователь 1С заводится
отдельный, с правами только на то, что нужно читать, — например `odata_user`. Пароль вводится в
терминале и на экране не отображается; он ложится в `bases.yaml` домашнего каталога открытым
текстом под правами владельца — сознательное решение: файл не покидает диск владельца, это не
сетевой секрет.

Роль базы задаёт умолчания записи и гейта:

| Роль | Уровень гейта | Запись | Проведение документов | Пометка удаления | Удаление записей независимых регистров | Лимит коммитов за 10 минут |
|---|---|---|---|---|---|---|
| `prod` | `identifiers+names` | нет | да | да | нет | 20 |
| `test` | `identifiers` | да | да | да | нет | 50 |
| `dev` | `off` | да | да | да | да | без лимита |

Роль — только набор умолчаний: любое поле записи базы его перекрывает (например, `role: prod` +
`write: true` — боевая база с разрешённой записью). Флаги записи из последних четырёх столбцов
действуют, только когда запись включена (`write: true`) — у базы без записи они значения не имеют.

Уровень гейта: `off` — гейт выключен, модель видит значения как в 1С; `identifiers` — реквизиты
(ИНН, счета, телефоны и подобные) идут токенами, названия и ФИО открыты; `identifiers+names` —
вдобавок названия организаций и ФИО тоже токенами. Готовая запись базы выглядит так (адрес,
пользователь и пароль — плейсхолдеры):

```yaml
bases:
  ut_test:
    label: УТ 11, тестовая
    url: https://server/base/odata/standard.odata/
    user: odata_user
    password: qwerty123
    role: test
    config: ut
```

### Файлы настроек

Все настройки шлюза хранятся в домашнем каталоге `~/.claude/odata1c/`. Создавать эти файлы
вручную не нужно: каждый появляется при выполнении соответствующей команды и уже содержит
закомментированный образец всех допустимых полей с пояснениями. Те же образцы лежат в репозитории,
в каталоге `src/odata1c/templates/`, и по ним удобно заранее посмотреть, что и как настраивается.

| Файл в домашнем каталоге | Что в нём настраивается | Когда создаётся | Образец в репозитории |
|---|---|---|---|
| `bases.yaml` | перечень баз: адрес публикации, пользователь и пароль 1С, подпись, роль, уровень защиты, разрешения на запись | `odata1c init` или первый запуск шлюза; записи добавляет `odata1c base add` | [bases.example.yaml](src/odata1c/templates/bases.example.yaml) |
| `daemon.yaml` | порт шлюза, лимиты, поведение с клиентами, которые не умеют подтверждать запись | `odata1c init` или первый запуск шлюза | [daemon.example.yaml](src/odata1c/templates/daemon.example.yaml) |
| `bases/<база>/policy.yaml` | что именно скрывать в этой базе: скрытые сущности, открытые поля, собственные классы защиты | `odata1c base add` | [policy.example.yaml](src/odata1c/templates/policy.example.yaml) |
| `bases/<база>/recipes.yaml` | рецепты этой базы | `odata1c base add` с ключом `--recipes` | [recipes/ut.yaml](src/odata1c/templates/recipes/ut.yaml) |
| `recipes/<конфигурация>/<имя>.yaml` | библиотека рецептов, общая для баз одной конфигурации | сохранением рецепта в разговоре с моделью | пример в разделе [«Рецепты»](#рецепты) |

Чаще всего правится `bases.yaml`. В записи базы меняют подпись (`label`), по которой модель
выбирает базу, роль (`role`), разрешение записи (`write`), уровень защиты (`gate.mode`), состав
разрешённых операций (`permissions`) и конфигурацию 1С (`config`), от которой зависит библиотека
рецептов. Изменения в `bases.yaml` и `policy.yaml` действуют со следующего запроса модели,
перезапуск шлюза не требуется. Политику защиты надёжнее менять не в редакторе, а командами
`odata1c policy`: они проверяют имена сущностей и полей по метаданным базы.

Остальное содержимое домашнего каталога служебное, и править его не следует. Это
`policy.auto.yaml` (разметка защищаемых полей, которую шлюз строит сам по метаданным базы),
`launcher.key`, файлы `*.sqlite` (индекс метаданных, словарь токенов, журнал записей) и каталог
`logs/`.

**Файл правит владелец, не модель.** Модель его штатно не читает и не правит — хук плагина
`PreToolUse` перехватывает такие обращения к домашнему каталогу шлюза и требует решения человека.
Это дополнительная защита, а не единственная: реальные значения не выходят через MCP ни при каком
обращении, а пароль 1С модели попросту не нужен — этого достаточно, даже если бы хука не было.
Базу заводит владелец сам, в своём терминале — просить модель «пропиши базу» бессмысленно, у неё
нет для этого инструмента. Подробнее — [docs/install.md](docs/install.md).

Первый реиндекс долгий: у типовой УТ `$metadata` — это около 17 МБ описания и больше семи тысяч
сущностей. Дальше индекс пересобирается, только если у публикации изменилась контрольная сумма
`$metadata`.

Теперь можно спрашивать в Claude Code:

- «какие базы 1С мне доступны?» — модель вызовет `odata1c_bases`;
- «найди справочник контрагентов и покажи состав его полей» — `odata1c_find_entity`,
  затем `odata1c_describe_entity` с классами гейта у каждого поля;
- «возьми любого контрагента и покажи его пять последних заказов клиента» — `odata1c_query`;
  название контрагента придёт токеном, номера и суммы документов — как есть.

Что модель видит и в каком порядке ходит — навык `odata1c` из плагина; справочные темы об
устройстве OData 1С, токенах, политике и протоколе записи — тул `odata1c_info`.

## Что внутри

Два процесса из одного пакета: **демон** (`odata1c daemon`) — единственный на машину, держит
соединения с базами, индекс, словарь и журнал, отвечает по MCP Streamable HTTP на
`127.0.0.1:7171`; **лаунчер** (`odata1c mcp`) — тонкий stdio-процесс на сессию, который поднимает
демон при необходимости и проксирует ему вызовы. Собственной логики у лаунчера нет.

### Тулы чтения

| Тул | Что делает | Когда нужен |
|---|---|---|
| `odata1c_bases` | список видимых баз: подпись, роль, уровень гейта, статус индекса | в начале новой сессии — узнать, какие базы доступны |
| `odata1c_find_entity` | нечёткий поиск справочника, документа или регистра по названию | когда точное имя сущности неизвестно |
| `odata1c_describe_entity` | состав полей сущности: типы, ключи, связи, классы гейта | перед выборкой — чтобы выбрать нужные поля |
| `odata1c_query` | выборка записей сущности: отбор, сортировка, страницы | основной способ читать данные — списки, поиск, фильтры |
| `odata1c_get` | один объект по ключу | когда ключ объекта уже известен |
| `odata1c_info` | справочник по устройству OData 1С и самого шлюза, по темам | разобраться в токенах, политике, порядке записи |
| `odata1c_reindex` | обновить индекс метаданных базы | 1С отвечает «сущность не найдена» на объект, который точно есть, или после обновления конфигурации |
| `odata1c_raw_get` | произвольный запрос по пути публикации | когда `query` и `get` не выражают нужное обращение |
| `odata1c_recipe` | список готовых запросов базы или выполнение одного из них | вместо того чтобы собирать сложную выборку (остатки, задолженность) вручную |

### Тулы записи

Пишущие тулы ничего не пишут сами: каждый готовит операцию и показывает превью в токенах. В 1С
пишет только `odata1c_commit` — и только после подтверждения пользователя. Любую выполненную
запись можно откатить тулом `odata1c_undo`.

| Тул | Что делает | Когда нужен |
|---|---|---|
| `odata1c_create` | подготовить создание объекта — справочника или документа | завести новую запись в базе |
| `odata1c_update` | подготовить изменение полей существующего объекта | поправить значения по ключу |
| `odata1c_mark_for_deletion` | подготовить пометку удаления (или её снятие) | «удалить» объект — в 1С это всегда пометка, не физическое удаление |
| `odata1c_delete_record` | подготовить физическое удаление записи независимого регистра сведений | единственный тул с настоящим удалением — только для регистров без регистратора |
| `odata1c_action` | подготовить проведение или отмену проведения документа | провести или распровести документ |
| `odata1c_commit` | выполнить подготовленную операцию в 1С | после того как пользователь увидел превью и подтвердил запись |
| `odata1c_undo` | подготовить откат уже выполненной записи | отменить результат коммита — по его `commit_id` |
| `odata1c_journal` | последние выполненные записи с исходом и способом подтверждения | посмотреть историю записи или найти `commit_id` для отката |

## Командная строка

Команды `odata1c` — для владельца машины, не для модели. Общий ключ `--home <путь>` (или
переменная окружения `ODATA1C_HOME`) у любой команды меняет домашний каталог шлюза.

**Базы**

| Команда | Что делает |
|---|---|
| `odata1c init` | создать домашний каталог и шаблоны настроек |
| `odata1c base add <имя> [--role prod\|test\|dev] [--recipes ut\|bp\|zup]` | добавить базу — адрес, подпись, пользователя и пароль спросит сама |
| `odata1c base import <путь>` | перенести базы из env-файла прежнего сервера |
| `odata1c base list` | список описанных баз |
| `odata1c base test <имя>` | проверить соединение с базой |
| `odata1c reindex <имя> [--force]` | обновить индекс метаданных базы |

**Политика гейта**

| Команда | Что делает |
|---|---|
| `odata1c policy show <имя>` | показать действующую политику базы |
| `odata1c policy check <имя>` | проверить файл владельца по индексу |
| `odata1c policy hide <имя> <сущность> [--yes]` | скрыть сущность целиком, вместе с дочерними |
| `odata1c policy open <имя> <Сущность.Поле>` | открыть поле (класс `keep`) |
| `odata1c policy set <имя> <Сущность.Поле> <класс>` | назначить полю класс защиты |

**Рецепты**

| Команда | Что делает |
|---|---|
| `odata1c recipe check <config>` | проверить библиотеку рецептов конфигурации |
| `odata1c recipe list <имя>` | список рецептов базы с источником каждого |

**Служебные**

| Команда | Что делает |
|---|---|
| `odata1c doctor [--online]` | проверка окружения: Python, `uv`, дом, базы, индекс, демон, Claude Code (`--online` — ещё и соединение с базами) |
| `odata1c daemon [--foreground]` | запустить демон вручную (обычно его поднимает лаунчер сам) |
| `odata1c daemon stop` | остановить демон |
| `odata1c mcp [--bases ...] [--default ...] [--url ...]` | лаунчер — то, что прописывается в `.mcp.json`; руками обычно не вызывается |
| `odata1c reveal <токен> [--base ...] [--field ...]` | реальное значение токена — только для владельца, в терминале |
| `odata1c --version` | версия пакета |

## Рецепты

Рецепт — это заранее составленный и проверенный запрос к базе, которому дано имя и у которого
объявлены параметры. Остатки товаров на складе, задолженность покупателей, выручка за период:
всё это типовые вопросы, и ответ на каждый из них в 1С требует знать, в каком регистре лежат
данные, как называется его виртуальная таблица и какие у неё поля. Рецепт хранит это знание в
готовом виде.

Без рецептов модель каждый раз заново исследует структуру базы и собирает запрос с нуля. Это
занимает несколько обращений к 1С и не защищает от ошибки в имени регистра или поля. С рецептом
тот же вопрос решается одним вызовом, и каждый раз одинаково, потому что запрос уже сверен с
метаданными конфигурации.

От пользователя рецепты ничего не требуют. Достаточно спросить обычными словами, например
«покажи остатки по основному складу на сегодня». Модель запрашивает у шлюза перечень рецептов
базы, выбирает подходящий и выполняет его со своими параметрами, в этом примере с датой и
складом. Для этого служит один инструмент, `odata1c_recipe`: вызов без имени возвращает перечень
с описаниями и параметрами, вызов с именем выполняет рецепт.

Рецепты собираются из трёх источников. Если имя встречается в нескольких, действует рецепт из
источника, который стоит в списке выше.

1. **Рецепты базы** лежат в файле `bases/<база>/recipes.yaml` и действуют только для неё. Здесь
   уместны запросы, которые учитывают доработки конкретной базы.
2. **Библиотека конфигурации** лежит в каталоге `~/.claude/odata1c/recipes/<конфигурация>/`, по
   одному файлу на рецепт, и общая для всех баз этой конфигурации. К какой конфигурации относится
   база, определяет поле `config` в её настройках: `ut`, `bp`, `zup` или собственное обозначение.
3. **Стартовый набор из поставки** копируется в базу ключом `--recipes` команды `odata1c base add`.
   Сейчас он заполнен для «Управления торговлей».

Библиотека пополняется в ходе обычной работы. Когда запрос, найденный в разговоре, дал нужный
результат и пригодится снова, достаточно попросить модель сохранить его как рецепт. Навык плагина
`odata1c-recipe` заменит конкретные значения параметрами и запишет файл в библиотеку конфигурации.
Команда `odata1c recipe check <конфигурация>` сверяет рецепты библиотеки с метаданными базы, а
`odata1c recipe list <база>` показывает все рецепты базы с указанием источника каждого.

Рецепт записывается обычным файлом YAML. В нём есть описание, сущность 1С, параметры и сам запрос.
Данных базы в нём нет: ни значений, ни токенов.

```yaml
title: Остатки по складу
description: Количество в наличии на указанный момент.
entity: AccumulationRegister_ТоварыНаСкладах_Balance
params:
  period: { type: datetime, required: true, description: момент остатков }
  warehouse: { type: guid, required: false, description: Ref_Key склада }
virtual:
  Period: "{period}"
  Condition:
    - Склад_Key eq {warehouse}
select: [Номенклатура_Key, ВНаличииBalance]
filter: ВНаличииBalance gt 0
top: 200
```

Библиотека задумана как растущая: чем дольше шлюз работает с базой, тем больше вопросов
решается одним вызовом. В планах поставлять наборы рецептов для типовых конфигураций вместе с
плагином и обновлять их вместе с ним.

## Ограничения

- **Физического удаления объектов нет.** «Удаление» для объектов и подчинённых регистров — только
  пометка удаления (`DeletionMark = true`). Из тулов записи физически удаляет только
  `odata1c_delete_record`, и только записи независимых регистров сведений.
- **Через `odata1c_update` нельзя вписать открытым текстом телефон, почту, адрес или дату
  рождения** — только токеном или при создании объекта (`odata1c_create`): иначе ответ «изменений
  нет» позволял бы подбирать чужие данные перебором значений.
- **Сокращённое название в свободном тексте проходит открытым.** Словарь знает полные написания
  названия; если в комментарии документа человек написал узнаваемое сокращение, которого нет в
  справочнике, гейт его не заменит. Ловить «ядро» названия отвергнуто: ядра — обычные слова, их
  замена портила бы данные ложными срабатываниями.
- **Аутентификации у демона нет.** Он слушает только `127.0.0.1`; модель угроз — диск и машина
  владельца. Публикации по сети с аутентификацией и TLS пока нет.
- **Записи в регистры, подчинённые регистратору, нет** ни при каком разрешении: их пишет
  проведение документа, а не прямая запись.
- **Шаблоны рецептов заполнены только для УТ.** Для БП и ЗУП шаблоны пока пустые: базы для сверки
  имён нет, а невыверенный рецепт хуже пустого.
- **Проверен шлюз только с Claude Code.** Другие клиенты MCP по stdio (Cursor, VS Code, Claude
  Desktop) подключаются и читают; там, где нет диалога разрешения Claude Code или elicitation,
  запись по умолчанию выключена (`write_confirm_fallback: deny`). Навыки, хук и агент — часть
  плагина Claude Code, в других клиентах их нет. Cowork запускает MCP-серверы в изолированной
  среде без доступа к демону на `127.0.0.1` — с ним шлюз не работает.

## Документы

| Файл | Что внутри |
|---|---|
| [docs/install.md](docs/install.md) | установка на Windows и Linux, обновление, `doctor`, типовые сбои |
| [CHANGELOG.md](CHANGELOG.md) | что вошло в выпуск |
| [CONTRIBUTING.md](CONTRIBUTING.md) | для тех, кто хочет разрабатывать шлюз |

## Лицензия

MIT.