Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "session-recall",
"version": "0.5.0",
"version": "0.6.0",
"description": "Shared semantic recall over local Claude Code, Codex, and Cursor history. Bundles MCP search tools, deep recall, guided setup, and a background freshness hook.",
"author": { "name": "Max Butorin" }
}
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "session-recall",
"version": "0.5.0",
"version": "0.6.0",
"description": "Shared semantic recall over local Claude Code, Codex, and Cursor history, with durable deep navigation and guided setup.",
"author": {
"name": "Max Butorin"
Expand Down
2 changes: 1 addition & 1 deletion .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "session-recall",
"displayName": "Session Recall",
"version": "0.5.0",
"version": "0.6.0",
"minClientVersions": {
"cursor": "2.5.0"
},
Expand Down
162 changes: 162 additions & 0 deletions docs/decisions/2026-08-06-team-hub-central-index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Командный хаб: центральный индекс вместо local-first

## Контекст

У Максима выросла команда мидлов, которые работают через агентов. Задача,
сформулированная им прямо: «мне стало трудно за ними следить, мне легче в их
историю глянуть». Плюс вторая, обратная по направлению: любой участник должен
мочь спросить «а как ты делал то или это» и получить ответ по истории команды,
а не дёргать человека в мессенджере.

Это ломает заявленный инвариант проекта. README говорит, что индекс и история
живут локально, а весь P2P-share (`2026-07-30-p2p-sharing-v1-security-gate.md`)
построен именно вокруг того, чтобы **не** централизовать: там ответ собирает
воркер на машине владельца, а наружу уходит только явно одобренный текст.

Развилка обсуждалась 25.07.2026 (сессия `07bc6547`), но тогда свернула в
сторону egress-туннеля: выяснилось, что Voyage отбивает домашний IP, и вопрос
«перенести ли сервис на сервер» подменился вопросом «как достучаться до
Voyage». Сейчас вернулись к исходному.

## Решение

Отдельный режим — **хаб**, включается наличием клиентского конфига. Без него
всё работает ровно как раньше (репозиторий публичный, solo-путь ломать
нельзя).

Три выбора, сделанные Максимом явно:

- **Общий пул.** Все ищут по истории всех. Не «своё + расшаренное».
- **Уезжает всё**, а не выбранные проекты.
- **Только сервер.** Локальный индекс в хаб-режиме не ведётся.

Ключевое архитектурное решение: **хаб — это обычный session-recall, у которого
транскрипты приходят по сети, а не от агента.** Сервер складывает присланное в
ту же раскладку каталогов, которая есть на ноутбуке
(`<owner>/claude/<project-dir>/<session>.jsonl`), и запускает по ней штатный
`index_corpus`. Никакого серверного экстрактора, второго формата хранения и
параллельного пути поиска.

Транспорт — stdlib `http.server` и `urllib`, как в relay, а не MCP-over-HTTP:
ключ живёт в клиентском файле `0600`, а не в конфиге агента; плагин у
сотрудника ставится без изменений; новых зависимостей ноль.

Маскировка секретов — **два слоя, ловящие разное**:

1. на клиенте, до отправки — регэкспы форматов (`share/scanner.redact`);
2. на сервере, до записи на диск — точное совпадение со значениями из Doppler,
замена на имя переменной: `${servers/NETCUP_PASSWORD}`.

Сервер хранит **хеши** значений под солью, а не значения.

Ответы на вопросы (`/v1/ask`) пишет Codex по подписке (`codex exec`), модель
`gpt-5.6-terra`.

## Почему

- **Регэкспы и Doppler ловят разные вещи.** `sk-ant-…` узнаётся по форме;
пароль формы не имеет и ловится только точным совпадением. Ни один слой не
покрывает второй, поэтому их два.
- **Хеши вместо значений.** Иначе `claude-recall` становится копией всего
Doppler в памяти процесса, и его компрометация отдаёт разом все секреты
команды — ровно то, чего централизация и так добавляет риска.
- **Фильтр по длине и энтропии обязателен.** В Doppler рядом с ключами лежит
`DOPPLER_CONFIG=dev`. Слепая замена всех значений вырезала бы слово «dev» из
истории всей команды. На живом прогоне из 650 секретов маскируются 348.
- **Учёт принятых байт отдельно от размера файла.** Маскировка меняет длину, и
если считать по размеру файла на диске, докачка хвостов разъедется на первом
же замаскированном ключе.
- **`owner` берётся из ключа, а не из тела запроса.** Иначе любой участник
может писать в историю другого.
- **`--ephemeral` у Codex — не гигиена, а безопасность.** Хаб индексирует то,
что хранит; сохранённая сессия воркера положила бы вопрос коллеги в общий
индекс, откуда он позже читается как «наша прошлая работа» — stored-инъекция,
описанная в гейте P2P.
- **Промпт — второй слой, не первый.** Правило «не разглашай личное» задано
промптом по просьбе Максима, но нагрузку несёт маскировка: вырезанного текста
модель не разгласит в принципе, а инструкцию можно обойти.
- **Согласие сотрудника — не формальность.** Скрытый мониторинг это и правовая
проблема, и практическая: он вскроется и обойдётся дороже открытого. Экран
согласия в `hub join` стоит одну команду.

## Что протестировали

- **Voyage с Netcup напрямую** — `http=200`, 265 мс. Значит серверу не нужен
SOCKS-туннель, а сотрудникам не нужен ключ Voyage вообще.
- **Раскладка «как на ноутбуке» реально индексируется штатным кодом** — залили
настоящий транскрипт по HTTP, `hub index` его переварил, поиск нашёл.
- **Маскировка на живом Doppler** — 650 секретов прочитано, 348 маскируется;
реальные `VOYAGE_API_KEY` и `NETCUP_PASSWORD` заменились на имена переменных,
в карте их нет.
- **Общий пул** — второй сотрудник нашёл историю первого.
- **Инкрементальность** — повторный `push` отправляет ноль байт.
- **Подписка, а не API** — с заведомо битым `OPENAI_API_KEY` и несуществующим
`OPENAI_BASE_URL` композер всё равно ответил.
- **`--ephemeral`** — сессий Codex на диске было 1344 и осталось 1344.
- **`ask` целиком** — вопрос → поиск по чужой истории → written-ответ с точной
переменной окружения и честным «фрагменты не отвечают на…».

Два бага поймали тесты до деплоя: валидатор путей отбивал ведущий дефис и
зарубил бы **каждый** claude-транскрипт (`-Users-egor-proj`), а `sk-ant-…`
помечался как `[REDACTED:openai-key]`, потому что паттерн OpenAI шире и стоял
раньше.

## Чего тесты поймать не могли (правки по итогам первого боевого корпуса)

Первая настоящая заливка 3.5 ГБ вскрыла три вещи, невидимые на фикстурах.

**Скорость.** 256 КБ на чанк давали ~0.1 МБ/с: узкое место не байты, а
одно TLS-рукопожатие и один fsync на чанк, потому что `urllib` не
переиспользует соединение. 4 МБ → ~60 МБ/мин, история укладывается в час
вместо девяти.

**Маскировка резала конфигурацию.** Отбор по длине и энтропии не отличает
`claude-opus-4` от токена — обе строки короткие и разноклассовые. По корпусу
замаскировались 851 упоминание имени модели, 2429 hostname и 1536 IP: искать
«какую модель брали» стало невозможно, читать историю — тяжело. Решает **имя**
переменной, а не форма значения: `PASSWORD|TOKEN|SECRET|KEY|CREDENTIAL` — креды,
`*_MODEL|*_HOSTNAME|*_IP|*_REGION|*_BUCKET` — конфигурация. Из 650 секретов
Doppler маскируется 200 вместо 348.

**Авто-редакт съедал текст.** `password-assignment`
(`\w*(token|secret|key)\s*[=:]\s*\S{8,}`) сработал 27324 раза против ~1900 у
всех настоящих форматов ключей вместе взятых: он ловит любое поле JSON и любую
фразу вида «token: …». Для `scan` это нормально — находку читает человек и
ложное срабатывание бесплатно. Для автоматического вырезания оно удаляет
работу, а не секреты, поэтому в `redact` попадает только явный allowlist
форматов.

Общий урок: **у флагующего и у режущего детектора разная цена ошибки**, и один
набор паттернов не может обслуживать оба. Корпус после этих правок перезалит
заново.

## Отвергли

- **Своё + расшаренные проекты** — выбор Максима в пользу общего пула; следить
за командой иначе не выходит.
- **Гибрид «локальный + серверный индекс» со слиянием выдачи** — два разных
эмбеддера дают несравнимые скоры, склейка получилась бы эвристикой.
- **MCP-over-HTTP напрямую из агента** — ключ уехал бы в конфиг агента, плагин
пришлось бы переделывать, а упавший сервер давал бы агенту транспортную
ошибку вместо внятной фразы.
- **Postgres + pgvector** — на текущем объёме (36k чанков на человека) SQLite с
sqlite-vec справляется; переезжать, когда упрёмся.
- **Маскировка хранением значений** — быстрее и ловит секреты с разделителями,
но превращает сервис в хранилище всех секретов команды.
- **Anthropic API для ответов** — Максим выбрал подписку Codex; ключ не нужен.

## Открытые вопросы

- `codex exec --sandbox read-only` не запрещает чтение файлов: инъекция во
фрагменте теоретически может попросить модель прочитать что-то с сервера.
Ограничение остаётся на уровне unix-пользователя сервиса.
- `project` в выдаче берётся из `cwd` транскрипта, поэтому для сессий во
временных каталогах получается мусорная метка. Поведение унаследовано от
solo-режима.
- Удаление истории уволившегося: отзыв ключа закрывает доступ, но данные
остаются. Команды `hub purge <owner>` пока нет.

---

06.08.2026 · реализация в ветке `claude/session-recall-server-6171d6`,
модуль `src/session_recall/hub/`. Инструкция — `docs/team-hub.ru.md`.
Loading
Loading