Как настроить хуки OpenCode V2 и проверить вызовы. Хуки OpenCode V2 настраивают в локальном серверном плагине. В ветке 2.x хук execute.after получает один изменяемый объект события; в V1 использовались отдельные объекты input и output. Реализацию V1 нужно переписать для V2. Ниже — учебный плагин, ключ корреляции и таблица диагностики от загрузки до фактического пути вызова.
Ветка 2.x и запрет на перенос V1
Документация OpenCode по миграции прямо предупреждает: реализации плагинов V1 в V2 не запускаются, перемещения файла или переименования записи в конфиге недостаточно. В V1 плагин возвращал один объект с инструментами и хуками, где ключом служила строка вроде tool.execute.after. В V2 плагин экспортирует по умолчанию определение с id и setup(ctx), а хуки регистрируются через контекст домена. Поэтому переносить возвращаемый объект tool.execute.after в V2 бессмысленно: там нет механизма, который его прочитает.
Таблица миграции из документации сопоставляет старые ключи с новыми API. Для инструментов указано: tool.execute.before соответствует ctx.tool.hook("execute.before", ...), а tool.execute.after — ctx.tool.hook("execute.after", ...). Это адреса переноса. Код нужно переписать на регистрацию в setup, иначе хук останется в старом файле и не сработает.
Конфигурацию документация предлагает нормализовать автоматически: ключ plugin заменяется на plugins, а кортеж «пакет и опции» — на объект с полями package и options. Локальные плагины V2 размещают в .opencode/plugins/. Настроенный локальный путь обязан указывать на каталог: файл ./plugin/local.ts пропускается с предупреждением в логе, а реализацию переносят в ./plugin/local/index.ts.
Серверная поверхность плагина
Серверная часть плагина исполняется в сервере OpenCode и добавляет инструменты, хуки, интеграции, команды и агентов. Клиент подключается к этому серверу, поэтому хук наблюдает операции, прошедшие через серверный процесс. Контекст плагина устроен как клиент сервера OpenCode: методы чтения и действий принимают те же входы и ответы, что и клиент, плюс добавляются методы для трансформаций, рантайм-хуков, перезагрузок и регистраций.
setup выполняется при загрузке плагина и может вернуть функцию очистки, которая срабатывает при выгрузке. Внутри setup доступны ctx.location.directory и ctx.location.project.canonical — расположение экземпляра плагина. Документация отдельно оговаривает: это расположение самого плагина, к которой он имеет доступ, и не каждого события, которое получает. Для журнала вызовов это значит, что путь загрузки и путь конкретного вызова — разные вещи, и их стоит различать в диагностике.
Хуки перехватывают живые операции. Хук регистрируется на домене, который владеет операцией, а возвращённая регистрация освобождается, когда перестаёт быть нужна. Несколько плагинов могут зарегистрировать один и тот же хук; OpenCode запускает их в порядке плагинов, поэтому поздние хуки видят изменения ранних.
Локальный call-audit.ts
Ниже — один локальный плагин .opencode/plugins/call-audit.ts с экспортом по умолчанию Plugin.define и стабильным id. Идентификатор нужен каждому плагину V2: хранилище плагина привязано к этому ID, и он же опознаёт плагин в статусе и диагностике.
import { Plugin } from "@opencode/plugin"
type AuditRecord = {
time: string
phase: "before" | "after"
sessionID: string
id: string
tool: string
status?: "completed" | "error"
}
export default Plugin.define({
id: "local.call-audit",
async setup(ctx) {
const write = (record: AuditRecord) => {
console.log(JSON.stringify(record))
}
await ctx.tool.hook("execute.before", (event) => {
write({
time: new Date().toISOString(),
phase: "before",
sessionID: event.sessionID,
id: event.id,
tool: event.tool,
})
})
await ctx.tool.hook("execute.after", (event) => {
write({
time: new Date().toISOString(),
phase: "after",
sessionID: event.sessionID,
id: event.id,
tool: event.tool,
status: event.status,
})
})
},
})
Поле id в событиях — это Tool.CallID из типов плагина, то есть идентификатор конкретного вызова инструмента. В объяснении его удобно называть callID, но в коде берётся именно event.id. Оба хука регистрируются через await, поэтому регистрация завершается до того, как плагин начнёт наблюдать вызовы.
Поля журнала и приватность
Учебный журнал хранит шесть полей: time, phase, sessionID, id, tool, status. Поля input, result и содержимое сообщений в него не попадают. В типах execute.before и execute.after присутствует поле input, а у завершённого вызова — result, и именно их учебный пример намеренно не пишет. Аргументы инструмента и результаты могут содержать пути, фрагменты файлов и другие данные, которые в диагностический журнал попадать не должны.
status появляется только в фазе after. В типах execute.after это объединение: либо status: "completed" с полем result, либо status: "error" с полем error. Запись фазы before поля status не имеет, и это нормально: до выполнения инструмента итог ещё неизвестен.
| Поле | before | after | Назначение |
|---|---|---|---|
time |
есть | есть | Момент записи |
phase |
before |
after |
Этап хука |
sessionID |
есть | есть | Сессия вызова |
id |
есть | есть | Идентификатор вызова |
tool |
есть | есть | Имя инструмента |
status |
нет | completed или error |
Итог выполнения |
Корреляция по sessionID и id
Записи сопоставляются по паре sessionID и id. Имя инструмента tool в ключ корреляции не входит: два вызова read в одной сессии — это два разных вызова с разными id. Если сопоставлять по имени инструмента, журнал склеит разные операции и покажет ложную картину.
Учебный пример корреляции из принятого артефакта выглядит так: запись before с sessionID: "demo-session", id: "demo-call-7", tool: "read" и запись after с теми же sessionID и id, тем же tool и status: "completed". Это условный вход для разбора. Пара sessionID и id совпала — значит, обе записи относятся к одному вызову.
| sessionID | id | tool | before | after.status |
|---|---|---|---|---|
demo-session |
demo-call-7 |
read |
есть | completed |
demo-session |
demo-call-8 |
read |
есть | error |
demo-session |
demo-call-9 |
read |
есть | нет записи |
Третья строка показывает случай, когда парной записи after в журнале нет. Такой пропуск сам по себе не доказывает ошибку инструмента: запись могла не дойти по другой причине, и вывод об ошибке из одного пропуска делать нельзя.
Состояния: загрузка, before, completed, error
Диагностика разводит пять состояний. Загрузка плагина — есть ли id и источник в списке активных плагинов. before — зарегистрировался ли хук и дошёл ли вызов до него. completed — запись after со статусом завершения. error — запись after со статусом ошибки. Отсутствие парного after — отдельное состояние, которое не приравнивается к ошибке инструмента.
Порядок проверки идёт от общего к частному. Сначала подтверждают, что плагин загружен и его id виден в списке активных. Затем проверяют, что хук execute.before зарегистрирован и записывает вызовы. После этого смотрят, появляется ли парная запись after и какой у неё status. Пропуск after разбирают отдельно, сопоставляя его с моментом выгрузки плагина и с тем, дошёл ли вызов до серверного хука.
Документация по миграции предлагает проверять перенесённый плагин по шагам: подтвердить, что его ID и источник видны в списке активных плагинов; прогнать каждый зарегистрированный хук, трансформацию, инструмент, команду и подписку на события; проверить очистку перезагрузкой или удалением плагина; протестировать опции и сохранённое состояние на чистом проекте. Для опубликованного пакета советуют тестировать установленную версию.
Таблица диагностики
Таблица ведёт от версии и источника загрузки к регистрации и фактическому пути вызова. Каждая строка — отдельная проверка со своим признаком и выводом.
| Шаг | Что проверяем | Признак | Вывод |
|---|---|---|---|
| 1 | Версия ветки | 2.x | V1-код не переносится |
| 2 | Источник загрузки | .opencode/plugins/ или запись в plugins |
Плагин найден |
| 3 | ID в списке активных | local.call-audit |
Плагин загружен |
| 4 | Регистрация хуков | await в setup |
Хуки активны |
| 5 | Путь вызова | запись before |
Вызов дошёл до хука |
| 6 | Итог вызова | after.status |
completed или error |
| 7 | Парная запись | after отсутствует |
Отдельное состояние |
Строка 2 опирается на правила загрузки: плагины в .opencode/plugins/ загружаются автоматически, а опубликованные пакеты и каталоги из других мест добавляют в plugins в opencode.json(c). Одна запись загружает все части плагина. Строка 3 использует список активных плагинов, доступный через контекст. Строки 5 и 6 опираются на записи журнала, а строка 7 фиксирует пропуск как самостоятельный случай.
Упражнение на учебном проекте
Упражнение выполняют на отдельном учебном проекте, где нет рабочих данных. Задача — увидеть две записи об одном вызове безопасного чтения файла. Опасную команду ради проверки статуса error запускать не нужно: состояние ошибки разбирают по типам событий.
Порядок действий:
- Создайте отдельный каталог проекта и файл
.opencode/plugins/call-audit.tsс кодом выше. - Откройте проект в OpenCode и убедитесь, что
local.call-auditвиден в списке активных плагинов. - Дайте агенту задачу прочитать обычный текстовый файл внутри проекта.
- Найдите в выводе две записи с одинаковыми
sessionIDиid. - Сверьте
phaseиstatusвторой записи. - Сравните две записи по таблице корреляции.
Ожидаемый результат — пара записей, где before идёт без status, а after несёт status: "completed". Если парной записи нет, зафиксируйте это как пропуск и проверьте шаги 2–4 таблицы диагностики. Если запись after несёт status: "error", посмотрите на сам факт ошибки, не выводя содержимое error в журнал.
Границы наблюдения
Хук execute.before и execute.after наблюдает вызовы инструментов, прошедшие через этот хук в серверном процессе. Приложение, его политика безопасности и правила разрешений — другие уровни, и журнал вызовов их не заменяет. Запись в журнале подтверждает факт прохождения вызова через хук.
Правила разрешений в OpenCode устроены отдельно: ctx.permission.assert оценивает правила сессии, включая сохранённые одобрения, и запрашивает пользователя, когда правило сводится к вопросу. Хук evaluate проверяет решения о разрешениях после оценки настроенных правил и до выполнения действия или публикации запроса. Явный запрет в конфигурации окончателен и хук не вызывает. Эти механизмы живут на своём уровне, и учебный журнал их не дублирует.
Событие session.idle относится к подписке на поток событий сервера. Строить вывод о завершении вызова на нём не стоит: для этого есть парная запись after с её status. Подписка на события — отдельная возможность контекста, и её поведение описывается в документации по событиям.
Частые вопросы
Как связать записи before и after одного вызова?
Ключ корреляции — пара sessionID и id. Оба поля присутствуют в событиях execute.before и execute.after. Имя инструмента в ключ не входит, потому что два вызова одного инструмента имеют разные id.
Почему хук execute.after не срабатывает?
Возможные причины стоит проверять по порядку: плагин не загружен, хук не зарегистрирован в setup, вызов не дошёл до серверного хука. Пропуск парной записи сам по себе не доказывает ошибку инструмента. Начните с шагов 2–5 таблицы диагностики.
Можно ли перенести код V1 без изменений?
Нет. Реализации V1 в V2 не запускаются, а возвращаемый объект с ключом tool.execute.after в V2 не читается. Хуки регистрируют через ctx.tool.hook в setup, а конфигурацию переводят с plugin на plugins.
Какие поля безопасно писать в журнал?
time, phase, sessionID, id, tool и status. Поля input, result и содержимое сообщений в учебный журнал не пишут: они могут нести данные проекта. status появляется только в фазе after.
Заменяет ли журнал правила безопасности?
Нет. Журнал фиксирует вызовы, прошедшие через хук инструментов. Правила разрешений и политика приложения работают на своих уровнях, и явный запрет в конфигурации хук evaluate не вызывает.
