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