CH 21 · Plugins Hook e Intercepción: Manipular Antes de la Ejecución de la Herramienta
Objetivo del capítulo
En CH 20 dejamos que el modelo llamara a su propia herramienta. Este capítulo va más allá: cuelga tu propia lógica antes y después de que la herramienta se ejecute — registra quién llamó qué, intercepta herramientas que no deberían llamarse, decide permitir o denegar.
Esto son "plugins hook". Es la base del sistema de permisos, sandbox y capacidades de auditoría de dsh, y la encarnación más típica de "todo es un plugin".
Las llamadas a herramientas no son una línea recta
En CH 11 mencionamos un concepto: cuando el modelo dice llamar a una herramienta, no se ejecuta directamente, sino que pasa por un pipeline extensible. El equipo oficial lo convirtió en un "pipeline protegido" — cada etapa puede ser interceptada y mejorada por plugins.
Una llamada a herramienta pasa por estas etapas:
El modelo quiere llamar a una herramienta
↓
pre-execute → Compuerta de política: allow / deny / ask (permiso, sandbox, intercepción todo aquí)
↓
guard → Guardia monotónica: una vez denegada, listeners posteriores no pueden revocar (línea de defensa final)
↓
execute → Ejecuta realmente la herramienta
↓
post-execute → Transformación del resultado: reescribe el valor de retorno, añade contenido
↓
result → Observación de solo lectura: echa un vistazo al resultado, no puede cambiar
↓
El resultado vuelve al modeloLos "Hooks" son plugins montados en una etapa concreta: usa ctx.on('tools/xxx', ...) para suscribirte al evento correspondiente, y haz lo que quieras en el evento.
Los docs oficiales tienen una tabla clara sobre lo que puede hacer cada punto de extensión:
| Punto de extensión | Efecto | Uso típico |
|---|---|---|
tools/pre-execute | Capa de decisión antes de la ejecución de la herramienta | Allow / deny / ask, compuerta de permiso |
ctx.tools.guard() | Denegación final monotónica | Límite duro que no puede ser revocado por listeners posteriores |
tools/execute | Envuelve todo el ciclo de dispatch | Añadir timeout, reintento, recolección de métricas |
tools/post-execute | Transforma explícitamente el resultado | Reemplazar contenido mostrado, añadir contexto visible para el modelo |
tools/result | Observación de solo lectura del resultado inmutable | Logs de auditoría, estadísticas, no puede cambiar |
pre-execute es un evento waterfall: tu listener puede devolver next() (permitir) o { kind: 'deny', reason: '...' } (denegar).
Práctica: escribe un plugin Hook de "Auditoría + Intercepción"
Escribiremos un plugin hook que tanto registra cada llamada a herramienta como deniega herramientas especificadas. La lista de denegación se hace configurable — practicando al mismo tiempo la capacidad "el plugin puede configurarse".
Paso 1: crea directorio, instala dependencias
En un workspace donde quieras poner plugins:
New-Item -ItemType Directory -Path "hook-demo\src" -Force
cd "E:\software-workspace\DeepSeek harness demo\hook-demo" # sustituye por tu directorio
npm init -y
npm install @deepseek-ai/schemasteryschemastery es la librería para definir schemas de configuración (cuando un plugin necesita ser configurable, usa esto para declarar la forma y los valores por defecto de la configuración). Recuerda añadir "type": "module" en package.json.
Paso 2: escribe el plugin Hook
Crea hook-demo\src\audit.js:
import Schema from '@deepseek-ai/schemastery'
export const name = 'audit-hook'
export const Config = Schema.object({
denyTools: Schema.array(Schema.string()).default([]),
})
export function apply(ctx, config) {
ctx.on('tools/pre-execute', (exec, next) => {
console.log(`[audit] Tool will be called: ${exec.name}`)
if (config.denyTools.includes(exec.name)) {
return { kind: 'deny', reason: `Policy: this session is forbidden from calling ${exec.name}` }
}
return next()
})
}Desglose línea por línea:
Config: declara los elementos configurables del plugin.denyToolses un array de cadenas, por defecto un array vacío. Elconfigenapply(ctx, config)es el resultado de fusionar la configuración del usuario y los valores por defecto.ctx.on('tools/pre-execute', ...): suscríbete al evento antes de la ejecución de la herramienta. Cada vez que una herramienta esté a punto de ser llamada, pasa por aquí.console.log: auditoría — log de que esta herramienta está a punto de ser llamada.config.denyTools.includes(exec.name): si esta herramienta está en la lista de denegación, devuelve{ kind: 'deny', reason }para interceptarla.return next(): en caso contrario, permite, deja que el pipeline continúe.
Paso 3: pasa la configuración en cordis.yml
Crea hook-demo\cordis.yml (sustituye la ruta por la tuya, recuerda %20 para los espacios):
- insert:
- id: audit
name: 'file:///E:/tu-workspace/hook-demo/src/audit.js'
config:
denyTools: ['pwsh']Aquí config añade pwsh (PowerShell) a la lista de denegación. Fíjate: el código del plugin no cambió ni un solo carácter, pero el comportamiento cambió — eso es exactamente para lo que sirve la configuración, y el principio de diseño oficial de "sin parámetros ajustables hardcodeados": los valores que se pueden cambiar en cordis.yml no deberían estar hardcodeados en el código.
Paso 4: ejecútalo y observa el efecto
Usa headless para ejecutar una vez, deja que el modelo llame a pwsh:
cd tu-directorio-de-workspace
dsh --profile headless --patch "./hook-demo/cordis.yml" "Use pwsh to run Get-ChildItem to list the current directory"Mi salida real de terminal:

Dos logs [audit] amarillos son nuestro hook registrando: el modelo primero llamó a skill, luego a pwsh — cada llamada a herramienta pasó por nuestro hook. Y pwsh está en la lista de denegación, así que fue deny'd, el modelo percibió la denegación, reportó proactivamente "this session is forbidden from calling pwsh", y ofreció una alternativa.
Un plugin, haciendo tanto auditoría (visible) como intercepción (controlable). Ese es el poder de los hooks.
Deja que dsh lo haga: un prompt lo resuelve
Es más rápido que dsh escriba este plugin. Envía directamente en el cuadro de entrada de la Web UI:
In my current workspace, help me write a hook plugin: print a log line before a tool is called, and be able to deny specified tools via configuration. Implement per the official spec, run a headless to verify it can record and intercept, and finally tell me the result.Irá a leer los docs oficiales por sí mismo, escribirá el plugin, verificará que tanto el registro como la intercepción funcionan. Tú solo verificas.

Este es el resultado de ejecutar yo realmente este prompt: primero expuso una versión de los puntos de implementación él mismo — los plugins solo pueden tener exports con nombre de name / inject / apply, inject: ['tools'] asegura que el registro de herramientas esté listo, usa ctx.on('tools/pre-execute', ...) para colgar el hook (consistente con el ejemplo oficial de compuerta de permisos), devuelve { kind: 'deny', reason } para denegar, await next() para permitir — incluso "la lista de denegación va por config, sin cambio de código" lo pensó por ti.

Este diagrama de trayectoria es su proceso de trabajo completo: write para escribir el archivo del plugin, pwsh para ejecutar su propio script de verificación (8 comprobaciones todas pasan), todo_write para actualizar la lista de tareas... cada uno es una llamada a herramienta.
Errores comunes
| Problema | Qué está pasando | Cómo manejarlo |
|---|---|---|
Olvidaste return next() | Evento waterfall sin next, el pipeline se atasca | pre-execute / post-execute deben devolver next() o una decisión |
| El modelo sigue reintentando tras deny | El modelo no sabe que esta herramienta no está permanentemente disponible | Escribe el reason claramente, el modelo lo verá y cambiará a un enfoque diferente |
| La configuración no surte efecto | config en cordis.yml es incorrecto | Comprueba que el nombre del campo y el tipo del schema coincidan |
Reporta Cannot find package '@deepseek-ai/schemastery' | El directorio del plugin no instaló dependencias | npm install @deepseek-ai/schemastery |
Lo que aprendiste en este capítulo
- [ ] Enuncias las principales etapas del pipeline de llamadas a herramienta (pre-execute / execute / post-execute / result)
- [ ] Usas
ctx.on('tools/pre-execute', ...)para escribir un plugin hook - [ ] Devuelves
{ kind: 'deny', reason }para interceptar una herramienta,next()para permitir - [ ] Usas
Config+ Schemastery para hacer configurable el plugin (lista de denegación) - [ ] Enuncias el principio de diseño de "sin parámetros ajustables hardcodeados"
