Complementos
El sistema de complementos de ZeroClaw te permite añadir capacidades al agente sin tocar el binario principal. Esta página explica la decisión tecnológica: de qué está hecho un complemento, por qué es WebAssembly y cómo el host mantiene contenido un componente no confiable. Las guías de abajo recorren la creación de cada tipo de complemento, volviéndose más técnicas a medida que bajas.
- Escribir un plugin de herramienta: una herramienta invocable que el modelo puede llamar. Empiece aquí; es el recorrido completo de ejemplo, desde una crate vacía hasta la herramienta instalada.
- Escribir un complemento de canal: una integración de plataforma de mensajería con toda la superficie de indicadores de capacidad.
- Escribiendo un complemento de memoria: un backend de almacenamiento que implementa el recuerdo atribuido al agente.
- Distribución de plugins: firma, registros y seguridad de instalación.
Los paquetes de habilidades en Markdown-only no son plugins, pero viajan a través de la misma maquinaria de manifiesto, firma e instalación; esa página vive junto con la documentación de Skills.
Para la vista del operador sobre el descubrimiento, la política de firma y la configuración, consulte Cómo funcionan los complementos. Para la referencia normativa del contrato, consulte Protocolo de complementos.
Por qué WebAssembly
Un plugin ejecuta código arbitrario de terceros dentro de un proceso que contiene tus claves de API, tu historial de conversaciones y acceso al shell. El límite de aislamiento tiene que ser real, no meramente consultivo. ZeroClaw usa el modelo de componentes WASI sobre wasmtime porque ofrece cuatro propiedades que ningún esquema de biblioteca dinámica o subproceso iguala a la vez:
- Aislamiento basado en capacidades. Un componente WebAssembly no tiene autoridad ambiental. No puede abrir archivos, sockets ni variables de entorno a menos que el host conecte explícitamente esa capacidad en su enlazador. El host de ZeroClaw construye cada almacén de plugins con un contexto WASI que no tiene preaperturas del sistema de archivos ni red (
PluginStateencrates/zeroclaw-plugins/src/component.rs). Lo que un plugin puede alcanzar es exactamente el conjunto de importaciones del host que declara su world, más lo que añadan los permisos de su manifiesto, y nada más. - Ejecución con medición de combustible. El motor está compilado con la medición de combustible habilitada, y cada llamada recibe un presupuesto de combustible nuevo y un plazo basado en el reloj de pared que incluye el trabajo del host cuyo resultado se espera. Un complemento que entra en un bucle infinito o espera indefinidamente falla; no puede bloquear al agente. Los límites de memoria, tablas e instancias se aplican mediante un limitador del almacén. Los cinco límites proceden de la configuración del operador (
plugins.limits.*) y se validan para que no sean cero, y no se puede construir un almacén sin ellos, por lo que ninguna ruta de carga puede producir un complemento sin aislamiento. - Una ABI tipada e independiente del lenguaje. El contrato entre el host y el plugin es un conjunto de archivos de interfaz WIT (
wit/v0/en el repositorio de ZeroClaw), no una API de Rust. El host genera sus enlaces a partir de esos archivos conbindgen!de wasmtime; un plugin genera los enlaces de invitado en imagen especular conwit-bindgenen Rust o la herramienta equivalente en cualquier lenguaje que compile a un componentewasm32-wasip2. Los registros, variantes, resultados y tipos opción cruzan el límite con sus tipos intactos. - Comportamiento idéntico al de los integrados. Cada tipo de plugin se adapta al mismo trait de Rust que usan las implementaciones propias: un plugin de herramienta se convierte en un
Tool(wasm_tool.rs), un plugin de canal en unChannel(wasm_channel.rs), un plugin de memoria en unMemory(wasm_memory.rs). El bucle del agente, la atribución, los recibos y la política de seguridad no ven ninguna diferencia.
Las piezas
Un complemento en disco es un directorio que contiene un manifiesto y un componente compilado:
~/.zeroclaw/plugins/
└── my-plugin/
├── manifest.toml # identity, capabilities, permissions, signature
└── my-plugin.wasm # wasm32-wasip2 component
El manifiesto declara dos cosas ortogonales:
- Capacidades: lo que el plugin es. Una o más de
tool,channel,memory,observer,skill(el enumPluginCapabilityencrates/zeroclaw-plugins/src/lib.rs). Cada capacidad WASM selecciona el mundo WIT que el componente debe exportar. La capacidadskilles la excepción: marca un paquete markdown skill bundle que utiliza la maquinaria de instalación, no código, y no necesita ningún componente. - Permisos: qué servicios del host puede alcanzar el código del plugin. La enumeración
PluginPermissiondel mismo archivo. Actualmente,config_read(los adaptadores de herramientas y canales reciben su propia configuración pública materializada a partir del esquema y validada, y pueden resolver secretos designados por el esquema en llamadas de servicio autorizadas) yhttp_clienttienen efecto conductual. El permiso HTTP es la concesión necesaria para los adaptadores que implementanwasi:httpsaliente: las herramientas y los canales habilitan esa superficie, mientras que la memoria deliberadamente aún no lo hace.config_readdebe estar emparejado con elconfig_schemadel manifiesto; cualquiera de los dos sin el otro se rechaza. El esquema acepta los permisos de sistema de archivos y de acceso a memoria, pero todavía no están respaldados por funciones del host, por lo que declararlos no concede nada.
Los mundos
wit/v0/ define un mundo por capacidad WASM. Cada mundo importa la interfaz de host logging, cuyos eventos log-record llegan al log estructurado que lleva la atribución de span del sitio de llamada del host, y exporta plugin-info (nombre y versión autoinformados) además de su interfaz principal:
| World | Exportaciones | Ciclo de vida del almacén |
|---|---|---|
tool-plugin | tool: nombre, descripción, esquema de parámetros, execute | Almacén nuevo en cada execute; importaciones con ámbito secrets |
channel-plugin | channel: configurar, send, poll-message, más 22 métodos condicionados por capacidades | Almacén precargado protegido por un mutex asíncrono, reabastecido en cada llamada; importaciones limitadas a config, secrets y inbound proporcionado por el host |
memory-plugin | memory: almacenar, recuperar, obtener, olvidar, más 11 métodos limitados por capacidad | Almacén cálido detrás de un mutex asíncrono, reabastecido en cada llamada |
Los mundos de channel y memory usan banderas de capacidad: una máscara de bits que el host lee una sola vez al cargar (get-channel-capabilities / get-memory-capabilities). Para cada bandera no establecida, el host usa el valor predeterminado del trait de Rust y nunca llama al export del plugin. Así es como el contrato WIT se mantiene aditivo: un nuevo método opcional es una nueva bandera más una nueva función, nunca una ruptura.
Modelo de ejecución
El host (crates/zeroclaw-plugins/src/component.rs) posee un único wasmtime::Engine asíncrono para el proceso. La carga depende del backend: una compilación con el JIT de Cranelift compila .wasm al cargarse; una compilación solo en tiempo de ejecución deserializa un .cwasm precompilado. Cada instanciación de plugin obtiene:
- un
Storeque contiene el contexto WASI aislado, la tabla de recursos, el contexto HTTP opcional y el presupuesto de combustible; - un
Linkercon exactamente las importaciones que requieren su mundo, sus permisos y el soporte de adaptadores incluyeloggingsiempre,secretspara herramientas y canales,configeinboundpara canales, ywasi:httppara adaptadores de herramientas y canales solo cuando el manifiesto concedehttp_client. Memory no crea ni un contexto HTTP ni un enlazador HTTP. Cada adaptador comprueba la coherencia entre su contexto y su enlazador al instanciarse (ensure_http_coherent).
Las llamadas a herramientas no tienen estado por diseño: WasmTool::execute crea un almacén nuevo, ejecuta la llamada y lo descarta. Los canales y los backends de memoria tienen estado por naturaleza, por lo que mantienen un único almacén activo durante la vida útil del plugin; el host lo recarga antes de cada llamada para que un plugin de larga duración obtenga un presupuesto completo en cada llamada en lugar de agotarlo con el tiempo. Una interrupción por fecha límite descarta el almacén activo en lugar de reanudar un estado del guest parcialmente desenrollado. Los canales vuelven a crear la instancia en la siguiente llamada; la memoria permanece no disponible hasta que su propietario la reconstruye. Durante una llamada de canal autorizada, config.get y secrets.get materializan como máximo una revisión de la configuración canónica de esa instancia admitida. El host descarta la vista cuando termina la llamada. Un plugin de canal conforme debe resolver ambos en cada punto de uso y no debe conservar la configuración devuelta ni el secreto en texto plano en el estado activo del guest. El host no puede imponer la no retención después de devolver los datos a código de confianza del guest.
El límite es de 32 bits: wasm32-wasip2 es el único destino de WASI Preview 2 que incluye la toolchain de Rust, y la ABI de componentes reduce los offsets a 32 bits independientemente del tamaño de palabra del host. Los valores grandes (los bytes de un adjunto de canal) cruzan por valor. Consulta la página del protocolo para ver por qué esta es una restricción upstream.
Estado actual del cableado
Tenga en cuenta qué está registrado de extremo a extremo frente a qué está completo en el host pero aún no es accesible desde un daemon en ejecución:
| Capacidad | Adaptador host | Cableado en tiempo de ejecución |
|---|---|---|
tool | WasmTool | Registrado de extremo a extremo; los complementos de herramientas descubiertos aparecen en el conjunto de herramientas del agente |
skill | cargador de Markdown | Registrado de extremo a extremo; las habilidades se cargan con espacio de nombres como plugin:<plugin>/<skill> |
channel | WasmChannel, completo y cubierto por pruebas unitarias | La construcción gestionada por el alias y la resolución de la configuración en tiempo de ejecución ya se incorporaron (#10146); el escuchador de host específico de cada proveedor que vuelca cada transporte en la cola inbound del canal queda para una tarea posterior |
memory | WasmMemory, implementa el trait completo Memory | El runtime aún no lo construye como un backend configurable |
observer | ninguno | PluginCapability::Observer está reservado; todavía no existe ningún mundo WIT ni adaptador |
Configuración
La configuración estática del host de plugins usa el mismo espejo del esquema que todo lo demás. Actualmente, los valores por instancia usan TOML genérico o zeroclaw config set; el esquema del manifiesto del plugin todavía no se representa como un formulario de zerocode o gateway. Ten cuidado al editar manualmente: un error de sintaxis en una sección (por ejemplo, [plugins.entries] cuando debería usarse [[plugins.entries]]) hace que actualmente toda la sección [plugins] no se pueda deserializar y se revierta silenciosamente a los valores predeterminados, que se vuelven a leer como plugins.enabled = false sin ninguna advertencia (registrado en la incidencia #8636). Las operaciones habituales:
# encienda el sistema
zeroclaw config set plugins.enabled true
# carga durante la ejecución los complementos de herramientas y habilidades detectados automáticamente (predeterminado: false)
zeroclaw config set plugins.auto_discover true
# donde se descubren los plugins (por defecto: ~/.zeroclaw/plugins)
zeroclaw config set plugins.plugins_dir /srv/zeroclaw/plugins
# política de firma: disabled | permissive | strict
zeroclaw config set plugins.security.signature_mode strict
# límites de sandbox por llamada
zeroclaw config set plugins.limits.call_fuel 1000000000
zeroclaw config set plugins.limits.call_timeout_ms 30000
zeroclaw config set plugins.limits.max_memory_mb 256
plugins.enabled = true activa el host de plugins, pero las capacidades de herramientas y habilidades detectadas automáticamente solo se cargan cuando plugins.auto_discover = true también está establecido. Esa marca es false de forma predeterminada (cerrada ante fallos), por lo que enabled = true por sí solo te proporciona los canales que declares en [channels.plugin.<alias>] y ninguna herramienta ni habilidad de plugin: un paquete de herramientas o habilidades puede listarse y mostrar info correctamente, pero no aportar nada en tiempo de ejecución. Las vinculaciones de canales explícitas reciben su nombre del operador en lugar de detectarse automáticamente, por lo que no necesitan auto_discover; la marca solo controla las herramientas y habilidades detectadas automáticamente.
La configuración por instancia se encuentra en plugins.entries, indexada mediante una cadena versionada zpi1_… derivada de la identidad del paquete, la capacidad y la vinculación, todas ellas propiedad del host. La instalación muestra y establece inicialmente las claves de la vinculación de herramienta predeterminada del paquete; zeroclaw plugin info <package> vuelve a mostrar esa clave de herramienta. Estas interfaces automáticas son exclusivas de las herramientas. La construcción de canales asociados a alias deriva la clave de su alias configurado real, en lugar de inventar una vinculación basada en el nombre del paquete. Ese flujo de ejecución se incorporó en #10146: un demonio construye ahora una instancia [channels.plugin.<alias>] declarada explícitamente y resuelve su configuración tipada a partir de ese alias. La visualización automática de la clave mediante plugin info y la inicialización durante la instalación de las instancias de canal siguen siendo tareas manuales hasta que se complete el proceso de concesión en #9584. Las claves de identidad completa permiten que distintos paquetes y entornos de capacidades reutilicen de forma segura alias como main sin compartir credenciales. Los valores canónicos para el operador son un mapa de cadenas marcado como secreto y permanecen cifrados en reposo (enc2:…). Un complemento que solicita config_read declara el único contrato de tipos del mapa en config_schema: un objeto cerrado de Draft 2020-12 cuyas propiedades de nivel superior usan explícitamente string, boolean, integer, number, array u object. Un consumidor de una herramienta o de un canal puede establecer x-secret = true en una propiedad de cadena de nivel superior; el host valida ese valor con el objeto completo, lo elimina de la configuración pública y lo pone a disposición únicamente mediante la importación secrets.get de la instancia autorizada. Las herramientas pueden leer secretos durante execute; los canales obtienen la configuración pública mediante config.get y los secretos mediante secrets.get durante configure y las llamadas operativas. Sin la concesión efectiva de config_read, cualquiera de las dos importaciones devuelve access-denied; la instanciación, el descubrimiento de metadatos estáticos, los fallos de resolución y el agotamiento del presupuesto de llamadas al host devuelven unavailable. Almacene las cadenas directamente; use texto escalar JSON para booleanos y números, y texto JSON para matrices y objetos. El host materializa y valida el objeto tipado resultante antes de utilizar el código invitado de la herramienta o del canal; los valores desconocidos, malformados o fuera de rango provocan un error en lugar de llegar al complemento. Los complementos de memoria aún no tienen una importación de configuración y no deben solicitar config_read hasta que se añada esa ABI.
Los autores de plugins anteriores a la versión 1.0 deben migrar explícitamente: un manifiesto que solicita config_read sin config_schema ya no se detecta. Añada un esquema cerrado que coincida con los valores actuales, actualice los invitados de herramientas o canales para deserializar JSON tipado en lugar de un mapa de cadenas, vuelva a compilar y a firmar porque el esquema está incluido en la firma. Las integraciones del host inyectan PluginHostServices, que encapsula un PluginConfigResolver, en lugar de un mapa de configuración propio. Cada marco autorizado de herramienta o canal materializa como máximo un ResolvedPluginConfig vinculado al ámbito, usa esa vista para cada lectura de configuración dentro del marco y la descarta cuando este finaliza. Un canal llama a config.get y secrets.get en el punto de uso, de modo que la configuración pública y la rotación de credenciales dentro del mismo vínculo lógico se ven como una sola revisión en la siguiente operación. Las exportaciones de identidad estática y capacidades se leen una sola vez durante la carga; cambiar la identidad del bot o de la cuenta, u otros metadatos estáticos, requiere reconstruir el ciclo de vida del canal. Migración a la configuración tipada es la receta paso a paso, incluida la decisión de aplicar esta exigencia sin una capa de compatibilidad.
Este es un formato de clave estricto anterior a la versión 1.0: no se consultan las entradas heredadas cuyo nombre solo coincide con el de un paquete o binding. Para un paquete de herramientas existente, ejecuta zeroclaw plugin info <package> para obtener su clave de instancia completa, cambia el nombre de la entrada antigua a esa clave y guarda la configuración. Las instalaciones nuevas de herramientas la inicializan automáticamente.
Los permisos efectivos se comprueban por separado de las solicitudes del manifiesto. Si se deniega config_read, el host valida un objeto vacío. Las propiedades obligatorias hacen que el inicio falle de forma segura. Cuando el objeto vacío es válido, una herramienta omite la clave vacía __config y las importaciones de configuración/secretos del canal devuelven access-denied. La lista canónica de campos y los valores predeterminados del host se encuentran en la Referencia de configuración; zeroclaw config list muestra los valores almacenados actuales.
Dónde está realmente el límite de confianza
El sandbox limita lo que puede hacer un plugin cargado; la política de firmas limita lo que se carga en absoluto. Ambas son decisiones del operador, y se combinan:
plugins.enabledfalse (el valor predeterminado): no se ejecuta ningún código de plugin, nunca.plugins.auto_discoverfalse (el valor predeterminado): las capacidades de herramientas y habilidades detectadas automáticamente no se cargan.plugins.enabled = truepor sí solo activa únicamente los canales que declares en[channels.plugin.<alias>]; las herramientas y habilidades se cargan solo cuandoauto_discover = truetambién está establecido.- Firma
strict: solo los componentes cuyo manifiesto lleve una firma Ed25519 válida de una clave de tu conjunto de confianza se cargan. - Plugin cargado: limitado por combustible, topes de memoria, WASI sin preapertura y el conjunto de importaciones con permisos controlados.
Lo que el sandbox no delimita es el comportamiento semántico de una herramienta que el modelo decide invocar: una herramienta con la concesión http_client y la superficie HTTP del adaptador de la herramienta puede enviar lo que el modelo le pase a donde su código decida. La política de firmas existe porque “qué código cargo” es la decisión que más importa; tómala de forma deliberada.