Написание плагина

From docs/ru/plugin-authoring.md at cc732c3 View on GitHub

Плагин реализует один трейт — Plugin из опубликованного крейта rtok-plugin-sdk. Это верно независимо от того, поставляется ли плагин внутри этого репозитория или живёт в чужом (D25), — контракт один, и никаких внутренних обходных путей нет. crates/rtok-plugin-sdk/examples/shrink.rs — полноценный плагин в одном файле; examples/hello_plugin.rs — то же самое, подключённое к настоящему хосту.

0. Нужен ли он вообще?

Проверьте plan.md §1 (каталог) и §0 (решения), а также порядок задач этого плагина в roadmap.md. Если метода там нет, добавьте его в ideas.md (исходный инструмент + плагин; Later, если это v0.2+) и сначала предложите изменение плана. Каждый плагин здесь пишется с нуля: он не должен вызывать через оболочку сторонний инструмент, линковать его или читать его данные (D6). Инструмент, который он заменяет, — это спецификация; research.md — доказательная база.

1. Трейт

use rtok_plugin_sdk::{
    Ctx, DashboardPage, Manifest, Plugin, PreToolDecision, PreToolUse, Surface,
};

pub struct MyPlugin;

impl Plugin for MyPlugin {
    // Обязательно: что это за плагин.
    fn manifest(&self) -> Manifest {
        Manifest { id: "<id>", surfaces: &[Surface::Hook], default_on: true }
    }

    // Обязательно: страница, которую отображают и `rtok web`, и `rtok tui` (D23).
    fn dashboard_page(&self) -> DashboardPage {
        DashboardPage::new("<Title>", "One sentence for the operator.", true)
    }

    fn pre_tool(&self, ev: &PreToolUse, cx: &Ctx) -> Option<PreToolDecision> {
        let cfg = cx.plugin_config::<crate::config::MySection>("<id>");
        let _ = (ev, cfg);
        None
    }
}

Два обязательных метода обязательны на этапе компиляции: плагин, который не говорит, что он такое и какую страницу показывает, не собирается. У каждого метода событий — pre_tool, post_tool, session_start, prompt_submit, pre_compact, mcp_tools, proxy_filter — есть реализация по умолчанию, которая ничего не делает, поэтому реализуйте только интерфейсы, объявленные в манифесте. В документации крейта есть таблица того, когда хост запрашивает каждый из них.

cx — это хост: cx.estimate(text, Class::Code), cx.record(&Measurement { .. }), cx.log(..), cx.plugin_config::<T>("<id>") для вашего собственного раздела [plugins.<id>] и cx.config::<T>("proxy") для редкого значения на уровне всего хоста. К сохранённому состоянию обращаются так же — cx.put_archive(..), cx.search_notes(..), cx.symbol_defs(..) — через трейты возможностей, к которым разыменовывается Ctx. Плагин никогда не обращается к базе данных хоста напрямую; если возможности не хватает, её намеренно добавляют в SDK, а не обходят.

2. Внутри этого репозитория: подключение (три однострочника)

  • Cargo.toml → [features]: добавьте <id> = [] и допишите его в default.
  • src/plugins/mod.rs: #[cfg(feature = "<id>")] pub mod <id>; и соответствующий v.push(Box::new(<id>::MyPlugin)); в all(). Позиция = порядок диспетчеризации.
  • src/config.rs → CATALOGUE: ("<id>", default_on) в той же позиции.

Тест registry_matches_catalogue падает, пока все три не согласованы. Импортируйте из rtok_plugin_sdk напрямую — crate::plugin реэкспортирует те же типы для собственного кода диспетчеризации хоста, но плагин указывает крейт, который он реализует.

3. Вне этого репозитория

[dependencies]
rtok-plugin-sdk = "0.0.1"

Реализуйте Plugin на основе крейта — три зависимости, никакого SQLite, никакого tree-sitter, никакого веб-сервера. Чтобы запустить его, соберите бинарник, который зависит от rtok и передаёт ваш плагин реестру: Registry::from_plugins(vec![Box::new(Mine)], &Config::load()?). Этот репозиторий не поставляет сторонних плагинов и адаптеров. v0.2 (P32) добавляет необязательный путь для того же контракта в виде .wasm в ~/.rtok/plugins/ только для rtok mcp / rtok proxy — никогда для rtok hook; см. crates/rtok-plugin-sdk/PLAN.md § v0.2 survey — WASM plugin host.

4. Документация рядом с кодом

  • src/plugins/<id>/PLAN.md — проектная заметка D15 (скопируйте docs/plugin-plan-template.md). Попадает в коммит перед первой задачей по реализации плагина. Обязательные заголовки, ≥ 3 рассмотренных альтернативы, одна строка Target: и одна Falsified by:. Проверяется cargo test plugin_plans.
  • src/plugins/<id>/README.md — что он делает, что заменяет, интерфейсы, механизм, ключи конфигурации, задачи плана, статус. Для пользователей.
  • src/plugins/<id>/AGENTS.md — файлы, которыми он владеет, инварианты, которые нельзя нарушать, допустимые зависимости, проверки. Для того (человека или модели), кто будет редактировать его следующим.

Скопируйте структуру у существующего плагина; README и AGENTS.md должны умещаться на одном экране.

5. Правила, которым подчиняется каждый плагин

  1. Fail open. При любой ошибке возвращайте None/пустое значение. Никогда не паникуйте на пути хука; диспетчер перехватывает паники, но паника всё равно остаётся ошибкой.
  2. Без потерь. Если вы что-то сокращаете, сначала вызовите cx.put_archive для оригинала и поместите id в Measurement::ref_id и в текст, который видит модель. Модель должна иметь возможность выполнить для него expand.
  3. Измеряйте. Каждое действие, которое меняет то, что видит модель, вызывает cx.record(&Measurement) с байтами до/после и оценкой токенов. Экономии без строки не существует.
  4. Бюджет. Всё для SessionStart/UserPromptSubmit возвращается как Injection и никогда не записывается в additionalContext напрямую. Текст должен быть побайтно стабильным между ходами.
  5. Скорость. Путь хука ≤ 10 мс p95 в релизной сборке. Один индексированный запрос к БД — нормально; обход файловой системы или сетевой вызов — нет.
  6. Никаких новых зависимостей без однострочного обоснования в сообщении коммита, и только из базового списка в plan.md §2.
  7. Не дублируйте. Переиспользуйте существующий хелпер или вынесите один общий хелпер на тот уровень, которому принадлежит поведение.

6. Тестирование

Модульные тесты живут в самом модуле. Вне дерева или для плагина, которому не нужно хранилище, используйте rtok_plugin_sdk::testing::MemoryHost — он хранит то, что плагин записывает и архивирует:

use rtok_plugin_sdk::testing::MemoryHost;
use rtok_plugin_sdk::Ctx;

#[test]
fn rewrites_recursive_ls() {
    let host = MemoryHost::new();
    let input = serde_json::json!({ "command": "ls -R src" });
    let ev = PreToolUse { tool_name: "Bash", tool_input: &input };
    assert!(MyPlugin.pre_tool(&ev, &Ctx::new(&host)).is_some());
    assert_eq!(host.recorded().len(), 1);
}

В этом репозитории crate::plugin::Runtime::in_memory("test") — настоящий хост без файла; используйте его, когда проверка касается того, что действительно попало в хранилище, а фикстуры в tests/fixtures/hooks/ — для реалистичных событий. Затем выполните проверку задачи из plan.md (порядок см. в roadmap.md), затем just check.

7. Выпуск

Сделайте коммит <task-id>: <title> в собственной ветке задачи и откройте для неё один пул-реквест (D16). В том же пул-реквесте отметьте задачу выполненной и перенесите её из plan.md в done.md с датой и выводом проверки. Реализованная работа, которая всё ещё числится в plan.md, не завершена.