Metadata-Version: 2.5
Name: odata1c-gate
Version: 0.1.0
Summary: Локальный MCP-шлюз к OData 1С:Предприятие с гейтом псевдонимизации
Project-URL: Homepage, https://github.com/Romandredan/odata1c-gate
Project-URL: Repository, https://github.com/Romandredan/odata1c-gate
Author: Roman Danilov
License: MIT
License-File: LICENSE
Requires-Python: >=3.12
Requires-Dist: ahocorasick-rs>=0.22
Requires-Dist: httpx2>=2.12
Requires-Dist: httpx>=0.27
Requires-Dist: lxml>=5.2
Requires-Dist: mcp>=2.2
Requires-Dist: pydantic>=2.7
Requires-Dist: pyyaml>=6.0
Requires-Dist: ruamel-yaml>=0.18
Requires-Dist: snowballstemmer>=2.2
Requires-Dist: uvicorn>=0.30
Provides-Extra: keyring
Requires-Dist: keyring>=25.0; extra == 'keyring'
Description-Content-Type: text/markdown

# odata1c-gate

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

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

**Статус: чтение и запись закрыты (M1, M2), идёт этап поставки M3.** Работают демон, лаунчер,
индекс метаданных, гейт всех уровней, девять тулов чтения и семь тулов записи; приёмка на живой
базе 1С пройдена и для чтения, и для записи ([docs/probes/M1d-live-check.md](docs/probes/M1d-live-check.md),
[docs/probes/M2-live-check.md](docs/probes/M2-live-check.md)). Этап M3 собирает всё это в пакет
PyPI и плагин Claude Code; первый выпуск — `0.1.0` ([CHANGELOG.md](CHANGELOG.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
```

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

**Две оговорки.** Первая: обе команды заработают начиная с выпуска `0.1.0` — пока тег не
опубликован, пакета `odata1c-gate` на PyPI нет, и `uvx` при первом запуске шлюза ответит отказом
(`.mcp.json` плагина закреплён на версии из репозитория). Вторая: репозиторий пока приватный,
`claude plugin marketplace add` клонирует его через git, поэтому на машине нужны учётные данные
git с доступом к нему. Открытие репозитория — отдельное решение владельца.

Отдельно ставится командная строка — она нужна владельцу базы, а не модели (описать базу, собрать
индекс, раскрыть токен, править политику гейта):

```text
uv tool install odata1c-gate
```

После этого команда `odata1c` доступна в терминале. Без установки то же самое запускается как
`uvx --from odata1c-gate odata1c <команда>`. Подробности, Linux, обновление и разбор типовых
сбоев — [docs/install.md](docs/install.md).

## Первые пять минут

```text
odata1c init                          # ~/.claude/odata1c/ с шаблонами настроек, права владельца
odata1c base add ut_test --role test --recipes ut   # спросит адрес, подпись, пользователя, пароль
odata1c base test ut_test             # проверить соединение с 1С
odata1c reindex ut_test               # разобрать $metadata и построить индекс метаданных
odata1c doctor                        # проверить окружение: uv, дом, базы, демон, Claude Code
```

Начинать с `init` обязательно: плагин домашнего каталога не создаёт — его делают либо эта команда,
либо лаунчер при первой сессии Claude Code. `doctor` на пустом месте честно ответит `FAIL` в
строке домашнего каталога, поэтому он и стоит последним; запускать его можно в любой момент.

Адрес базы — это адрес публикации OData 1С, он оканчивается на `/odata/standard.odata/`
(например `https://1c.example.local/ut/odata/standard.odata/`). Пользователь 1С заводится
отдельный, с правами только на то, что нужно читать, — например `odata_claude`. Роль базы задаёт
умолчания: `prod` — уровень гейта `identifiers+names` и только чтение, `test` — уровень
`identifiers`, `dev` — гейт выключен и запись разрешена; любое поле переопределяется явно.
Пароль ложится в `bases.yaml` домашнего каталога открытым текстом, файл закрывается правами
владельца (ADR-0014).

Первый реиндекс долгий: у типовой УТ `$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`, `odata1c_reindex`, `odata1c_raw_get`, `odata1c_recipe`.
Тулы записи: `odata1c_create`, `odata1c_update`, `odata1c_mark_for_deletion`, `odata1c_action`
(проведение и отмена), `odata1c_undo`, `odata1c_commit`, `odata1c_journal`.

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

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

- **Физического удаления объектов нет.** «Удаление» для объектов и подчинённых регистров — только
  пометка удаления (`DeletionMark = true`). `PUT` не используется.
- **Сокращённое название в свободном тексте проходит открытым.** Словарь знает полные написания
  названия; если в комментарии документа человек написал узнаваемое сокращение, которого нет в
  справочнике, гейт его не заменит. Ловить «ядро» названия отвергнуто: ядра — обычные слова, их
  замена портила бы данные ложными срабатываниями.
- **Аутентификации у демона нет.** Он слушает только `127.0.0.1`; модель угроз — диск и машина
  владельца. Публикация по сети с TLS и токенами — этап M4.
- **Записи в регистры, подчинённые регистратору, нет** ни при каком флаге разрешений: их пишет
  проведение документа, а не прямая запись.
- **Шаблоны рецептов заполнены только для УТ.** Для БП и ЗУП шаблоны пока пустые: базы для сверки
  имён нет, а невыверенный рецепт хуже пустого.
- **Клиент — Claude Code.** Claude Desktop и Cowork не поддерживаются (ADR-0012).

## Документы

| Файл | Что внутри |
|---|---|
| [docs/install.md](docs/install.md) | установка на Windows и Linux, обновление, типовые сбои, раздел сопровождающего |
| [CHANGELOG.md](CHANGELOG.md) | что вошло в выпуск |
| [SPEC.md](SPEC.md) | спецификация v0.2, 16 разделов — главный артефакт проекта |
| [CONTEXT.md](CONTEXT.md) | глоссарий: термины и запрещённые синонимы |
| [docs/adr/](docs/adr/) | 15 архитектурных решений (0001–0015), статус — во frontmatter |
| [AGENTS.md](AGENTS.md) | вводная для AI-агентов: архитектура, стек, инварианты, процесс |
| [CLAUDE.md](CLAUDE.md) | указатель для Claude Code поверх AGENTS.md |

## Структура репозитория

```text
src/odata1c/          пакет PyPI odata1c-gate: демон, лаунчер, CLI (SPEC §2.2, §11.1)
  config/             bases.yaml и daemon.yaml, роли, валидация, права файлов
  registry/           реестр баз, статус индекса, видимость по сессии
  client1c/           httpx-пул на базу, IBSession, семафор, маппинг ошибок 1С
  index/              парсер EDMX → metadata.sqlite, реиндекс, нечёткий поиск
  gate/               детекторы реквизитов, словарь, подмена в обе стороны, страж
  write/              разрешения, pending-операции, commit, журнал, undo
  recipes/            загрузка рецептов и рендеринг параметров в OData-литералы
  tools/              регистрация тулов, ресурсов, промптов, server instructions
  templates/          файлы-шаблоны в поставке: конфигурация и рецепты УТ/БП/ЗУП
  cli.py              команды odata1c: init, base, reindex, policy, recipe, doctor, reveal, daemon, mcp
  daemon.py           демон: тулы, ресурсы, промпт, MCP Streamable HTTP на 127.0.0.1
  launcher.py         лаунчер: stdio-прокси демону, подъём демона, проброс elicitation
plugin/               плагин Claude Code: манифест, .mcp.json, навыки, хук, агент, evals (SPEC §11.2)
.claude-plugin/       маркетплейс плагина для claude plugin marketplace add
tests/                unit / property / integration + образцы EDMX (SPEC §12)
tools/                bump_version.py, plugin_dev_copy.py, probes/ — скрипты проверок и приёмки
docs/adr/             архитектурные решения
docs/plans/           планы реализации по этапам
docs/probes/          отчёты технических проверок P1–P8 и приёмок на живой базе
```

## Что не попадает в репозиторий

`bases.yaml` хранит пароли 1С открытым текстом (SPEC §3.4), поэтому в git не коммитятся
конфигурация рабочей машины, env-файлы, базы SQLite (словарь, индекс, журнал), выгрузки
`$metadata` и логи — см. [.gitignore](.gitignore). В поставке живут только файлы-шаблоны
в `src/odata1c/templates/`, в тестах — урезанные образцы `$metadata`, не полные дампы.

## Лицензия

MIT.
