# docs-masked Локальное обезличивание документов перед отправкой в языковую модель — и обратная подстановка после ответа. Документ никогда не покидает машину в исходном виде. Персональные данные заменяются устойчивыми тегами (`#PERSON_1#`, `#PHONE_2#`, `#ADDRESS_1#`), в модель уходит только текст с тегами, а полученный ответ восстанавливается локально по сейфу соответствий. ``` документ ──▶ маска ──▶ контроль утечки ──▶ модель ──▶ обратная подстановка локально локально сеть локально ``` Работает как скилл для Claude Code, как MCP-сервер для любого другого агента и как обычная утилита командной строки. ## Как это работает **1. Маска.** Документ разбирается на текстовые фрагменты — абзацы, ячейки, узлы разметки. В каждом находятся персональные данные, каждое значение получает устойчивый тег. Один и тот же человек получает один и тот же тег по всему документу, включая падежные варианты и инициалы: «Иванов Иван Иванович», «Иванову» и «Иванов И.И.» — это один `#PERSON_1#`. **2. Контроль утечки.** Замаскированный текст повторно прогоняется через все детекторы плюс параноидальный проход: любой `@`, любая цепочка из семи и более цифр, любой телефоноподобный набор. Если что-то осталось — отправка **блокируется** исключением, а не предупреждением в логе. **3. Отправка.** Наружу уходит только текст с тегами. Единственная точка выхода в сеть — функция `llm.send()`, и она обязана вызвать проверку до запроса. Каждая отправка пишется в журнал `~/.pii_shield/egress.jsonl`: время, провайдер, модель, размер, sha256, статус проверки. Содержимое не пишется. **4. Обратная подстановка.** Ответ модели проходит через сейф: теги заменяются на оригиналы. Для ФИО подставляется восстановленный именительный падеж — если в документе человек упомянут только как «Кузнецову Ивану Петровичу», в ответе он станет «Кузнецов Иван Петрович». ## Установка ```bash git clone https://github.com/kpshinnik/docs_masked.git ~/.docs_masked/src cd ~/.docs_masked/src && ./install.sh ``` Скрипт поставит зависимости, положит скилл в `~/.claude/skills/docs-masked` и напечатает готовый фрагмент конфигурации MCP. Подробности и варианты — в [docs/INSTALL.md](docs/INSTALL.md). ## Подключение к агенту | Способ | Кому | Как | |---|---|---| | **Скилл** | Claude Code, Claude.ai | `./install.sh` либо `/plugin marketplace add kpshinnik/docs_masked` | | **MCP-сервер** | Cursor, Windsurf, Codex CLI, Continue, Zed, Cline, Claude Desktop | `python3 mcp_server.py` как stdio-сервер | | **CLI и правило** | всё остальное | команды в терминале плюс `templates/AGENTS-rule.md` в свой проект | Пошагово по каждому харнесу — [docs/HARNESSES.md](docs/HARNESSES.md). MCP-сервер написан без зависимостей: нужен только `python3`. Он отдаёт шесть инструментов — `mask_text`, `unmask_text`, `verify_text`, `scan_document`, `mask_document`, `unmask_document`. ## Использование ```bash docs-masked scan договор.docx # что будет скрыто docs-masked mask договор.docx # маска + сейф docs-masked report договор.docx --open # посмотреть глазами docs-masked ask договор.docx -p "Найди риски по срокам" docs-masked unmask договор.masked.docx --vault договор.docx.vault.json ``` ### Команды | Команда | Что делает | |---|---| | `scan FILE` | Показывает, что будет замаскировано. Файл не меняется, сеть не трогается. | | `mask FILE` | Обезличенная копия в том же формате плюс файл сейфа. | | `unmask FILE --vault V` | Возвращает оригиналы. | | `verify FILE` | Проверяет, что персональных данных не осталось. | | `ask FILE -p "..."` | Полный круг: маска → проверка → модель → восстановленный ответ. | | `report FILE` | HTML-страница ревью: каждая замена в контексте, значения закрашены. | | `selftest` | Самопроверка круговорота. | Полный список флагов — [skills/docs-masked/references/cli.md](skills/docs-masked/references/cli.md). ## Что распознаётся ФИО в любом падеже (русские, латиница, транслит), организации, адреса, почта, телефоны, паспорт и код подразделения, СНИЛС, ИНН, ОГРН, КПП, БИК, расчётные счета, банковские карты, IBAN, полисы ОМС, водительские удостоверения, автомобильные номера, IP-адреса, `@никнеймы`, даты рождения и выдачи документов, коды реквизитов (ОКТМО, ОКПО, КБК), плюс ваши собственные строки. Идентификаторы проверяются по-настоящему: контрольная сумма СНИЛС, контрольные разряды ИНН и ОГРН, алгоритм Луна для карт, mod-97 для IBAN. Полная таблица — [references/coverage.md](skills/docs-masked/references/coverage.md). ## Форматы | Формат | Чтение | Запись на место | |---|---|---| | `.txt` `.md` `.rst` `.log` `.tex` `.yaml` `.ini` | да | да | | `.docx` | да | да, с сохранением форматирования | | `.xlsx` `.xlsm` | да | да | | `.csv` `.tsv` | да | да | | `.json` | да | да | | `.html` `.htm` | да | да | | `.pdf` | да | по флагу `--pdf-redact`, с физическим вымарыванием | | `.rtf` `.doc` `.odt` | да | нет (только macOS, через `textutil`) | DOCX обходится по XML, а не через `document.paragraphs`: иначе теряются абзацы внутри полей контента и надписей — на настоящем договоре из-за этого пропадала целая колонка блока реквизитов. В таблицах заголовок колонки используется как контекст: ячейка `500100732259` сама по себе неотличима от случайного числа, а в колонке «ИНН» распознаётся уверенно. ## Python API ```python from pii_shield import ask_document res = ask_document("договор.docx", "Составь резюме и найди риски", provider="anthropic") print(res.answer) # имена уже восстановлены ``` Ручной контроль каждого шага: ```python from pii_shield import mask_text, assert_clean, unmask_text r = mask_text(raw) # r.text — с тегами, r.vault — сейф assert_clean(r.text) # LeakGuardError, если что-то осталось answer = call_model(r.text) # наружу уходит только маска final, unknown = unmask_text(answer, r.vault, mode="canonical") ``` Подробнее — [references/api.md](skills/docs-masked/references/api.md). ## Сейф соответствий Сейф — единственное, что связывает теги с оригиналами. Без него обратная подстановка невозможна. - Пишется рядом с документом как `<файл>.vault.json`, права `0600`. - Шифруется по флагу `--pass-env` (scrypt + Fernet). - Хранит каноничную форму, все встреченные варианты и журнал вхождений в порядке документа — благодаря журналу точное восстановление возвращает исходную словоформу, а не каноничную. - Внесён в `.gitignore`. Не коммитьте его. ## Точность и границы Инструмент устроен так, чтобы **ошибаться в безопасную сторону**: лучше замаскировать лишнее, чем пропустить. Что стоит знать: - **Скан-PDF без текстового слоя** не обрабатывается — нужен OCR. - **Однофамильцы без инициалов** получают отдельные теги, а не сливаются в одного человека. - **Голое число без подсказок** может быть не распознано как идентификатор — но параноидальный проход всё равно не выпустит такой текст наружу. - **Произвольные латинские имена** без славянских окончаний и без обращения (`Mr.`, `Dr.`) не распознаются: ловить любую пару заглавных слов дало бы больше вреда, чем пользы. На критичном документе стоит один раз посмотреть `docs-masked report` глазами. ## Разработка ```bash python3 -m pytest tests/ -q # тесты python3 -m pii_shield.cli selftest python3 samples/make_samples.py # пересоздать тестовые документы ``` Инварианты, которые нельзя ломать, перечислены в [AGENTS.md](AGENTS.md). Всё в `samples/` синтетическое; каталог `examples/` зарезервирован под ваши локальные документы и в репозиторий не попадает. ## Лицензия MIT.