tipo: referencia estado: aceptado última-revisión: 2026-07-17 se-relaciona-con:
- FND-001
- ADR-003
- crates/zeroclaw-plugins
Protocolo del complemento
Este documento define el protocolo entre el host de complementos de ZeroClaw y los componentes de complementos WASM.
Qué es un complemento
Un plugin es un componente WebAssembly autónomo que ZeroClaw carga en tiempo de ejecución para añadir una capacidad que el binario principal no incluye. Vive en su propio directorio bajo ~/.zeroclaw/plugins/, junto a un manifiesto que lo nombra y declara lo que proporciona. ZeroClaw lo descubre al iniciarse, lo verifica y conecta sus funciones exportadas al agente en ejecución para que se comporten como capacidades integradas: un plugin de herramienta aparece ante el modelo como simplemente otra herramienta invocable (WasmTool implementa el mismo trait Tool que una herramienta nativa), un plugin de canal se comporta como un canal de mensajería y un plugin de memoria como un backend de almacenamiento.
Un plugin puede proporcionar una o más de las capacidades definidas en PluginCapability (crates/zeroclaw-plugins/src/lib.rs): una herramienta invocable, un canal de mensajería, un backend de memoria, un backend de observabilidad o un conjunto de habilidades en Markdown. El caso de habilidad es especial: no incluye ningún WASM, solo un directorio skills/ de Markdown, por lo que es la única capacidad que omite el componente compilado.
¿Por qué construir uno?
- Extiende sin bifurcar. Agrega una herramienta o canal sin modificar el árbol de código fuente de ZeroClaw ni esperar una versión; el complemento es tuyo y se carga desde tu directorio de instalación.
- Comportamiento nativo. Un plugin cargado no es un complemento de segunda clase. El puente implementa las mismas características en tiempo de ejecución que usan los integrados, por lo que una herramienta del plugin se ofrece al modelo, se atribuye y se invoca exactamente igual que una de primera parte.
- Elección de lenguaje. El contrato es WIT y el WASI Component Model, no una API de Rust. Cualquier lenguaje que compile a un componente
wasm32-wasip2puede implementar un world. La guía desarrollada a continuación es Rust porque esa es la vía con más compatibilidad hoy, pero el límite en sí es agnóstico al lenguaje. - Aislado de forma predeterminada. El host carga cada complemento en un contexto de WASI sin preaperturas del sistema de archivos ni acceso de red implícito. Un complemento no puede acceder silenciosamente al host; obtiene exactamente las funciones del host conectadas a su entorno y nada más. El tráfico HTTP saliente es la única superficie de red que se puede abrir, y solo cuando el manifiesto concede
http_clienty ese adaptador de capacidades habilita explícitamente su límite HTTP probado. Los adaptadores de herramientas y canales lo hacen; la memoria todavía no. - Procedencia verificable. Los manifiestos pueden firmarse con Ed25519, y un operador puede exigir firmas de editores de confianza antes de que se cargue cualquier plugin.
Lo que un plugin no puede hacer (hoy)
Estos son límites reales del host actual, no preferencias de estilo. Conócelos antes de diseñar en torno a una capacidad que no existe.
logging, configuración tipada, secretos con alcance de instancia,http_clienty la entrada proporcionada por el host están integrados. Entre los permisos que puede declarar un manifiesto,config_readexpone la configuración pública del propio complemento, validada según su esquema. Un esquema de herramienta o de canal puede designar secretos que se excluyen de la configuración pública y se resuelven en llamadas de servicio autorizadas. Es necesario concederhttp_clientpara las solicitudes salientes mediantewasi:http, pero el adaptador de capacidades también debe optar por esa superficie del host. Los adaptadores de herramientas y canales sí lo hacen; la memoria permanece deliberadamente sin HTTP hasta que su límite de red tenga cobertura a nivel de componente. Los permisos de sistema de archivos y de acceso a memoria aún son aceptados por el esquema del manifiesto, pero no tienen efecto: sus funciones del host todavía no están registradas en el enlazador. Consulta más abajo Permisos e importaciones del host.- Sin red de host ambiente ni sistema de archivos. El contexto WASI no tiene preopens ni red ambiente, por lo que un plugin no puede abrir sockets sin procesar ni leer archivos del host a través de WASI ambiente. Una herramienta o plugin de canal con un permiso
http_clientobtienewasi:httpde salida porque esos adaptadores lo habilitan de forma explícita; no puede escuchar. Los plugins de canal que deben recibir tráfico entrante no abren un listener por sí mismos: el host ejecuta el listener y envía los mensajes a través de la importacióninbound, que el plugin consume desde su exportaciónpoll-message. - Un límite de 32 bits. El objetivo es
wasm32-wasip2. La memoria invitada es un espacio de direcciones de 32 bits y la ABI de componentes reduce los desplazamientos a 32 bits independientemente del tamaño de palabra del host. Los valores grandes (por ejemplo, los bytes sin procesar de un adjunto de canal) cruzan el límite por valor. Consulte la sección de espacio de direcciones de 32 bits para ver por qué se trata de una restricción de la cadena de herramientas upstream, no de un flag que este repositorio pueda cambiar. - Una herramienta por complemento de herramienta. El mundo
tool-pluginexporta una única interfaztoolcon un nombre y un esquema. Un complemento que necesita exponer varias herramientas incluye varios componentes, o un mundo diferente. - Contrato experimental, no congelado.
wit/v0aún no lleva un marcador.frozen, así que las interfaces todavía pueden cambiar antes de la primera versión estable. Fija una versión y espera tener que recompilar tras un cambio de WIT.
Arquitectura
Los complementos de ZeroClaw son componentes de WebAssembly definidos por interfaces WIT en wit/v0/ y alojados mediante wasmtime directo (crates/zeroclaw-plugins). Un complemento se compila como un componente WASI Preview 2 (wasm32-wasip2) que exporta uno de los mundos de complemento (tool-plugin, channel-plugin, memory-plugin) e importa las interfaces del host declaradas por ese mundo en wit/v0/.
El host se encuentra en crates/zeroclaw-plugins/src/component.rs. Mantiene un único wasmtime::Engine con soporte asíncrono, genera los enlaces del mundo con wasmtime::component::bindgen! a partir de wit/v0 e integra una interfaz WASI p2 aislada en el enlazador de cada mundo. El estado del host por almacén (PluginState) contiene un WasiCtx creado sin preaperturas y sin red, además de la ResourceTable que requiere WASI, su ámbito emitido por el host y manejadores tipados de servicios activos. Todos los mundos importan logging; las herramientas importan secrets, mientras que los canales importan config, secrets e inbound. Un permiso http_client concedido adjunta y enlaza wasi:http. Las declaraciones de los mundos y el ámbito admitido siguen siendo los contratos canónicos de esa interfaz (consulta Importaciones del host).
Los tres puentes mundiales mapean cada mundo WIT a los traits nativos del runtime:
| World | Módulo puente | Superficie de ejecución |
|---|---|---|
tool-plugin | runtime.rs, wasm_tool.rs | zeroclaw_api::tool::Tool |
channel-plugin | wasm_channel.rs | rasgo de canal |
memory-plugin | wasm_memory.rs | rasgo de backend de memoria |
Los plugins de herramientas usan un almacén nuevo en cada llamada (sin estado). Los plugins de canal y memoria mantienen un almacén persistente protegido por un mutex asíncrono durante toda la vida del plugin.
Los complementos de herramientas se descubren y registran de extremo a extremo: el tiempo de ejecución recorre la contraparte para herramientas de channel_plugin_details() y construye un WasmTool para cada uno. El adaptador de host del canal (WasmChannel, su control de habilitación de wasi:http, los servicios de configuración en el punto de uso y la cola inbound alimentada por el host) está completo y cuenta con cobertura de pruebas unitarias, y PluginHost::channel_plugin_details() expone los complementos de canal respaldados por wasm para registrarlos. El tiempo de ejecución ahora resuelve una vinculación [channels.plugin.<alias>] declarada explícitamente, construye su WasmChannel y lo registra usando el alias configurado; esa construcción basada en alias y la resolución de la configuración del tiempo de ejecución se incorporaron en #10146. La tarea de seguimiento restante es el escuchador del host por proveedor, que drena cada transporte en la cola inbound del canal. El puente de memoria (WasmMemory) se encuentra en la misma situación, un paso más atrás: el adaptador implementa el trait completo Memory frente al mundo memory-plugin, pero el host todavía no expone una contraparte de memoria de channel_plugin_details() y el tiempo de ejecución aún no construye un WasmMemory como backend configurable.
Estructura del complemento
Un plugin es un directorio que contiene:
my-plugin/
manifest.toml # Plugin metadata and permissions
plugin.wasm # Compiled WASM module (optional for skill-only plugins)
Los complementos se descubren desde ~/.zeroclaw/plugins/ (configurable mediante plugins.plugins_dir en la configuración).
Búsqueda e instalación en el registro
La ruta local de instalación del complemento sigue siendo la fuente de verdad para los complementos instalados. Un registro es solo un índice JSON usado en tiempo de ejecución del comando para descubrir y descargar un archivo del complemento:
zeroclaw plugin search calendar
zeroclaw plugin install team-calendar
zeroclaw plugin install team-calendar@0.2.0
zeroclaw plugin search calendar --registry https://example.invalid/registry.json
zeroclaw plugin install team-calendar --registry https://example.invalid/registry.json
zeroclaw plugin search obtiene metadatos del registro y compara la consulta con los nombres y descripciones de los plugins. No instala, habilita ni ejecuta código de plugin.
zeroclaw plugin install <name> resuelve el nombre desde el registro, descarga el archivo zip seleccionado, verifica el resumen SHA-256 opcional, extrae el archivo de forma segura y luego entrega el directorio del plugin extraído a la ruta existente PluginHost::install. Las instalaciones desde rutas locales no cambian:
Cuando no hay una versión fijada, ZeroClaw elige la última entrada coincidente en el índice del registro, por lo que los publicadores del registro deben ordenar intencionalmente los nombres repetidos.
zeroclaw plugin install ./my-plugin
zeroclaw plugin install ./my-plugin/manifest.toml
La URL predeterminada del registro es:
https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw-plugins/main/registry.jsonPara registros privados o en etapas, usa --registry <url> por comando o establece ZEROCLAW_PLUGIN_REGISTRY_URL.
Las entradas del registro usan esta estructura:
{
plugins: [
{
"nombre": "team-calendar",
versión: "0.8.5",
"descripción": "Programar reuniones en un calendario de equipo",
author: "Example Team",
capabilities: [herramienta],
"url": "https://example.invalid/team-calendar-0.2.0.zip",
"sha256": "sha256:<hex digest del zip>"
}
]
}
El archivo debe contener o bien un manifest.toml en la raíz o un único directorio de complemento anidado que contenga manifest.toml. Los archivos con rutas de traversal, rutas absolutas, rutas de Windows con prefijo de unidad o más de un manifest se rechazan antes de la instalación. Las descargas tienen un límite mientras se transmiten, por lo que un servidor sin Content-Length no puede obligar a ZeroClaw a almacenar en búfer un archivo comprimido de tamaño excesivo. La extracción también tiene un límite, de modo que un archivo comprimido no puede expandirse sin límite en el área temporal de instalación.
La búsqueda es descubrimiento no autenticado. La instalación es el límite de seguridad: las instalaciones desde el registro usan la política de firma de plugins configurada y las claves de editor de confianza, igual que las instalaciones locales de plugins mediante PluginHost::install.
Estructura de plugin solo de habilidades (paquete markdown)
Un plugin cuya única capacidad es skill incluye skills en un directorio skills/ con formato agentskills.io y omite wasm_path:
my-toolkit/
manifest.toml # declares the skill capability, no wasm_path
README.md # optional bundle-level overview
skills/
design-review/
SKILL.md
scripts/
references/
code-review/
SKILL.md
data-analysis/
SKILL.md
references/
Cada SKILL.md debe incluir frontmatter YAML con los campos name y description; el runtime rechaza los paquetes cuyas skills omitan cualquiera de los dos en el momento del descubrimiento, en lugar de en la primera invocación. Las skills se registran con IDs con espacio de nombres de plugin con el formato plugin:<plugin-name>/<skill-name> (p. ej. plugin:my-toolkit/design-review) para evitar colisiones con las skills creadas por el usuario y entre paquetes.
Formato del manifiesto
El manifest es el archivo llamado manifest.toml en el directorio del plugin. Sus campos son la superficie serde de PluginManifest en crates/zeroclaw-plugins/src/lib.rs, que es la fuente de verdad:
| Campo | Obligatorio | Significado |
|---|---|---|
name | Sometimes the most powerful thing you can do is set a boundary. “I can’t take this on right now” is a complete sentence. | Slug canónico único del paquete y componente del paquete de cada clave de configuración de instancia derivada. No es en sí una clave de configuración del operador. Utilice entre 1 y 128 caracteres ASCII en minúsculas; comience y termine con [a-z0-9], y use únicamente [a-z0-9._-] entre ellos. El descubrimiento rechaza los nombres no válidos o duplicados. |
version | Sometimes the most powerful thing you can do is set a boundary. “I can’t take this on right now” is a complete sentence. | Cadena de versión, p. ej. 0.1.0. |
description | no | Descripción legible para humanos mostrada por zeroclaw plugin list. |
author | no | Nombre del autor u organización. |
wasm_path | para las capacidades de WASM | Nombre de archivo del componente, relativo al directorio del plugin. Obligatorio a menos que la única capacidad sea skill. La detección omite el plugin si el archivo especificado no existe. |
capabilities | sí, no vacío | Qué es el plugin: cualquiera de tool, channel, memory, observer, skill (PluginCapability, serializado en snake_case). |
permissions | no | Servicios del host a los que puede acceder el código: http_client, config_read, file_read, file_write, memory_read, memory_write (PluginPermission). Actualmente solo se aplican los dos primeros; el resto se acepta, pero no tiene efecto. Declarar config_read requiere config_schema, y actualmente solo los adaptadores de herramientas/canales lo proporcionan. |
config_schema | exactamente con config_read | Borrador 2020-12 de JSON Schema para la configuración privada de este complemento; el esquema se incluye en los bytes del manifiesto canónico y, por tanto, queda cubierto cuando se firma el manifiesto. La raíz debe ser un objeto con un mapa de properties y additionalProperties = false. Cada propiedad de nivel superior debe tener un único tipo compatible explícito, directamente o mediante un puntero JSON local: string, boolean, integer, number, array u object. Los consumidores de herramientas y canales pueden establecer x-secret = true directamente en una propiedad de cadena de nivel superior para eliminarla de la configuración pública y exponerla mediante la importación del host con ámbito secrets.get. Las herramientas reciben la configuración pública en __config y pueden leer secretos durante execute. Los canales leen el objeto público actual mediante config.get y los secretos mediante secrets.get durante configure y las llamadas operativas; ambas importaciones no están disponibles durante la instanciación ni el descubrimiento de metadatos estáticos. Los marcadores de secreto anidados, falsos o no booleanos y las propiedades secretas cuyo tipo no sea cadena se rechazan. Se rechaza un esquema sin config_read, o un config_read sin esquema. |
signature | no | Firma Ed25519 en Base64url sobre los bytes canónicos del manifiesto. Se establece al firmar para distribución. |
publisher_key | no | Clave pública Ed25519 codificada en hexadecimal del firmante. |
Declare solo los permisos que el código realmente usa. Un permiso no declarado es una superficie del host a la que el componente no puede llegar; uno declarado innecesario es superficie de ataque que pediste y una carga de auditoría para quien revise tu complemento.
Los valores del operador siguen siendo cadenas en plugins.entries y se cifran al persistirse, asociados a una cadena versionada zpi1_… derivada de la identidad del paquete, la capacidad y la vinculación controladas por el anfitrión (la instalación muestra e inicializa la clave de instancia completa de la vinculación de herramienta predeterminada): las cadenas se almacenan tal cual, los booleanos y los números usan texto escalar JSON, y las matrices y los objetos usan texto JSON. Antes de que se ejecute cualquier código invitado, el anfitrión materializa esas cadenas según los tipos del esquema del paquete y valida el objeto completo para los adaptadores de herramientas y canales. Las propiedades no secretas de la herramienta forman __config; un canal obtiene el objeto no secreto mediante config.get. Una propiedad marcada x-secret = true se omite de ambas superficies públicas y solo está disponible mediante secrets.get("property") en un marco de servicio autorizado. Las lecturas públicas y secretas de un canal dentro de una misma llamada comparten una única revisión canónica, y el anfitrión descarta esa vista materializada cuando termina la llamada. Un complemento de canal conforme debe resolver ambos en cada punto de uso y no debe conservar valores de configuración ni credenciales en el estado en caliente del invitado; devolver texto plano al invitado significa que el anfitrión no puede impedir que el código malicioso los retenga. Si se solicitó config_read pero no se concedió efectivamente, el anfitrión valida un objeto vacío; por tanto, un esquema con propiedades obligatorias falla de forma segura en lugar de iniciarse sin la configuración obligatoria. Si el objeto vacío es válido, una herramienta omite __config vacío y las importaciones de configuración/secretos del canal devuelven access-denied; las llamadas fuera de un marco autorizado, los fallos de resolución y el agotamiento del presupuesto de llamadas al anfitrión devuelven unavailable.
Capacidades
capabilities es una lista no vacía de valores PluginCapability, definida en crates/zeroclaw-plugins/src/lib.rs (serializada en snake_case). Cada valor selecciona el mundo WIT que exporta el plugin (tool, channel, memory), nombra un backend de observabilidad (observer) o marca un paquete de habilidades solo en markdown (skill). Consulta la enumeración para el conjunto canónico; es la fuente de verdad y esta página no lo vuelve a enunciar.
Un manifiesto debe declarar al menos una capacidad. wasm_path es obligatorio para cada capacidad excepto en un plugin cuya única capacidad sea skill, que no lleva ninguna carga útil de WASM y se rechaza en el descubrimiento si omite un bundle válido skills/ (validate_manifest_shape en host.rs).
Permisos
permissions es una lista de valores de PluginPermission, también definidos en crates/zeroclaw-plugins/src/lib.rs. Lee el enum para el conjunto canónico.
Tenga en cuenta la brecha entre lo declarado y lo aplicado: en el host de componentes actual, config_read y http_client tienen efecto en el comportamiento. Solicitar config_read requiere un config_schema, y declarar ese esquema sin el permiso también se rechaza. Antes de usar un componente de herramienta o de canal, el host determina su concesión efectiva, materializa los valores del operador del complemento como JSON tipado y valida el objeto completo. runtime.rs elimina cualquier __config proporcionado por el llamador antes de inyectar valores no secretos validados en una llamada a una herramienta; las propiedades de tipo cadena directas del nivel superior marcadas con x-secret: true se omiten de la configuración pública y se leen mediante la importación secrets con ámbito del host. Las herramientas reciben ese servicio durante execute. Los canales reciben la configuración pública mediante config.get y los secretos mediante secrets.get durante configure y las llamadas operativas, mientras que la instanciación y la detección de metadatos estáticos siguen sin estar disponibles. http_client es una concesión necesaria, no una decisión de autoridad completa: el adaptador de capacidades también debe construir el contexto HTTP y enlazar wasi:http. Los adaptadores de herramientas y canales se habilitan explícitamente después de validar la concesión. El adaptador de memoria no lo hace deliberadamente, por lo que conceder http_client únicamente a un ámbito de memoria no añade ninguna superficie de red. Las variantes restantes (file_read, file_write, memory_read, memory_write) son aceptadas por el esquema del manifiesto, pero todavía no están conectadas a una importación del host: declararlas no concede nada por sí solas. Reservan los nombres para las funciones del host que las regularán (consulte Importaciones del host más abajo).
Interfaces WIT
El contrato del complemento es el conjunto de archivos WIT en wit/v0/, paquete zeroclaw:plugin@0.1.0. Cada elemento está detrás de @unstable(feature = plugins-wit-v0) hasta que el paquete se estabilice; consulte wit/VERSIONING.md para las reglas de compatibilidad. Las interfaces a continuación son un resumen orientativo; los archivos .wit son la fuente de autoridad para las firmas exactas.
Mundos
wit/v0/ define tres mundos, vinculados mediante bindgen! en component.rs. Cada uno importa logging (host) y exporta plugin-info además de su interfaz principal: tool-plugin exporta tool, channel-plugin exporta channel y memory-plugin exporta memory. Tool también importa secrets; channel importa config, secrets e inbound. Las exportaciones obligatorias (sin valores predeterminados) de cada mundo se enumeran en el comentario de documentación del mundo en su archivo .wit.
tool interfaz
wit/v0/tool.wit define la interfaz de una sola herramienta. El host llama a name, description y parameters-schema una vez al cargar, y luego despacha execute por cada invocación:
record tool-result {
success: bool,
output: string,
error: option<string>,
}
name: func() -> string;
description: func() -> string;
parameters-schema: func() -> json-string;
execute: func(args: json-string) -> result<tool-result, string>;
parameters-schema devuelve una cadena de JSON Schema presentada al LLM para la invocación de herramientas. execute recibe argumentos codificados en JSON que coinciden con ese esquema y devuelve un tool-result o una cadena de error. json-string es un alias de tipo string de wit/v0/types.wit; los llamadores producen JSON válido y los receptores lo analizan.
channel e memory interfaces
wit/v0/channel.wit y wit/v0/memory.wit definen superficies controladas por capacidades. El host llama a get-channel-capabilities / get-memory-capabilities una vez al cargar, y para cada indicador no establecido usa el valor por defecto del trait de Rust en lugar de llamar al plugin. Un plugin aún debe exportar cada función (basta con un stub que devuelva el valor por defecto documentado); el host simplemente nunca llama a las que tienen el indicador ausente. El valor por defecto al que se resuelve cada indicador no establecido está documentado en línea en el WIT junto a los indicadores *-capabilities, que son la fuente de verdad tanto para el conjunto de indicadores como para sus valores por defecto.
Banderas de capacidad
Los métodos opcionales se publicitan mediante flags channel-capabilities y flags memory-capabilities. Como las flags son una máscara de bits, se pueden agregar nuevos métodos opcionales a un paquete vN/ sin un cambio incompatible, junto con una nueva función @since. Quitar o renombrar una flag, función, campo o caso de variante es incompatible y requiere un nuevo directorio vN+1/.
Importaciones de host
Las funciones del host son importadas por el plugin y proporcionadas por el tiempo de ejecución. El enlazador de cada mundo conecta logging (mediante la implementación del host en component_logging.rs, vinculada junto con add_wasi en component.rs). Tool y channel conectan el servicio secrets con ámbito de instancia. channel también importa config para su objeto público tipado e inbound para la cola de mensajes alimentada por el host que vacía mediante poll-message. Los adaptadores de Tool y channel conectan wasi:http saliente solo después de que el ámbito admitido conceda http_client (PluginStoreSpec::with_granted_http y add_wasi_http en component.rs). Memory no proporciona ni el contexto ni la superficie del enlazador. Los permisos del sistema de archivos y de acceso a la memoria permanecen inertes: las funciones del host que los controlarían aún no se han conectado al enlazador. La autoridad ambiental de un plugin es el contexto de WASI (sin preaperturas, sin red ambiental), además de exactamente las importaciones del host que sus concesiones y las activaciones explícitas de los adaptadores habilitan conjuntamente.
Las importaciones propiedad de ZeroClaw comparten un presupuesto de seguridad fijo por cada marco de servicio distribuido por el host. El límite máximo canónico es MAX_HOST_CALLS_PER_FRAME en crates/zeroclaw-plugins/src/component.rs. Al agotarse, el registro no hace nada, el sondeo entrante informa vacío y las lecturas de configuración pública o secretos devuelven unavailable. Un nuevo marco restablece el presupuesto. Este límite es una política fija del host, no una configuración duplicada del operador.
inbound
wit/v0/inbound.wit es importado por el mundo channel-plugin. Un plugin de canal se ejecuta sin un listener propio, así que el host ejecuta el listener (un servidor webhook, un túnel del proveedor, un cliente de sondeo) y encola cada mensaje recibido. El plugin vacía la cola desde su export poll-message llamando a inbound-poll, con inbound-pending disponible para vaciar en lotes:
inbound-poll: func() -> option<host-inbound-message>;
inbound-pending: func() -> u32;
El lado del host posee un InboundQueue por canal; WasmChannel::inbound entrega una clonación a la tarea de escucha para que el tráfico en cola sea visible para el vaciado del complemento.
logging
wit/v0/logging.wit es importado por los tres mundos. Los plugins llaman a log-record para emitir eventos estructurados de vuelta al host:
log-record: func(level: log-level, event: plugin-event);
La llamada es de tipo fire-and-forget: no devuelve nada y el host (component_logging.rs) absorbe todos los errores, por lo que un fallo al escribir el registro nunca puede bloquear la ejecución del plugin. La entrega es asíncrona: la importación entrega el registro a una cola acotada del host, drenada por un hilo dedicado, y devuelve el control sin bloquear, por lo que un consumidor de registros lento o bloqueado nunca puede retener una exportación del guest más allá de plugins.limits.call_timeout_ms. La postergación no cambia el significado de un evento: cada registro captura el span del host vigente en el punto de llamada del guest y se escribe dentro de ese ámbito, de modo que la atribución de agente/canal/herramienta y la etiqueta terminal coinciden con la emisión en línea. El límite es un límite de memoria real, porque los campos del evento son cadenas sin límite que se copian a la memoria del host fuera del límite max_memory_mb del guest: un registro cuyos bytes controlados por el guest superen los 64 KiB se descarta en lugar de truncarse, y los registros en cola utilizan un presupuesto fijo de 8 MiB de bytes agregados que solo se libera después de escribir un registro. Una cola llena, un registro que supere el límite o un presupuesto agotado descartan siempre el registro más reciente; el hilo de drenaje informa del número acumulado de descartes después de cada escritura y al despertarse estando inactivo, por lo que la pérdida sigue siendo observable incluso cuando nunca llega otro registro aceptado después de los rechazados. plugin-action y plugin-outcome reflejan las taxonomías cerradas Action / EventOutcome de zeroclaw-log; no existe deliberadamente ninguna variante de escape. No llames directamente a wasi:logging: los eventos del plugin tendrían un formato incoherente y no llegarían a todos los destinos en los que escribe zeroclaw_log.
config
wit/v0/config.wit se importa mediante el mundo del canal. Devuelve el objeto actual validado según el esquema y sin secretos como JSON:
get: func() -> result<json-string, config-error>;
El objeto conserva los tipos declarados por config_schema; se omiten las propiedades marcadas como x-secret: true. El servicio está disponible durante configure y las exportaciones del canal operativo. Devuelve access-denied cuando la instancia admitida carece de la concesión efectiva de config_read. Las llamadas durante la inicialización del componente o el descubrimiento de metadatos estáticos, los fallos del resolutor o de la validación y el agotamiento del presupuesto de llamadas al host devuelven unavailable sin exponer detalles internos.
config.get es un acceso en el punto de uso, no una instantánea en el momento de carga. Un complemento de canal conforme debe llamarlo en cada operación que use la configuración y no debe conservar el objeto devuelto en el estado cálido del entorno invitado. Esta es una regla de conformidad del complemento: después de devolver JSON al código invitado de confianza, el host no puede impedir que un componente malicioso lo copie.
secrets
wit/v0/secrets.wit es importado por los mundos de herramienta y canal. El invitado solo proporciona un nombre de propiedad de nivel superior:
get: func(name: string) -> result<string, secret-error>;
El host deriva los permisos de paquete, de capacidad y de vinculación, así como los permisos efectivos, a partir del PluginInstanceScope admitido; ninguno de ellos es una entrada del huésped. Solo se pueden leer las propiedades de tipo cadena directas de nivel superior marcadas con x-secret: true en el esquema del manifiesto. Las herramientas pueden leerlas mientras el host despacha execute. Los canales pueden leerlas durante configure y en llamadas operativas como send, poll, health y las acciones condicionadas por capacidades. La inicialización de componentes y las exportaciones de metadatos estáticos devuelven unavailable sin resolver la configuración. Dentro de un mismo marco de servicio del canal, cada config.get y secrets.get usa una única revisión canónica resuelta de la configuración; ese marco se descarta en todas las rutas de salida. Por tanto, un plugin conforme observa conjuntamente la rotación pública/secreta de una misma vinculación en su siguiente operación. access-denied, not-found y unavailable no revelan deliberadamente ningún detalle del resolvedor ni del esquema. Las lecturas correctas devuelven el texto sin cifrar al huésped de confianza. El servicio impide la inyección de valores públicos y la selección entre instancias; no es un proxy de salida que mantenga el valor oculto para el código del plugin. Un plugin de canal conforme debe resolver los secretos en cada punto de uso y no debe conservar una segunda copia en el estado en caliente. El host no puede hacer cumplir la no retención después de devolver el texto sin cifrar.
Configuración por plugin (__config y config.get)
Permiso: config_read
Un complemento no lee las variables de entorno del proceso. Su manifiesto debe combinar config_read con un config_schema Draft 2020-12; cualquiera de los dos sin el otro da lugar a un manifiesto no válido. La raíz del esquema debe ser un objeto con un mapa properties y additionalProperties = false. Cada propiedad de nivel superior debe declarar uno de string, boolean, integer, number, array u object, directamente o mediante un puntero JSON local al paquete. Los consumidores de herramientas y canales pueden establecer x-secret: true en una propiedad de cadena directa de nivel superior; los marcadores anidados, con valor false o no booleanos, así como las propiedades secretas que no sean de tipo cadena, se rechazan. Las claves desconocidas, las codificaciones malformadas y las infracciones de las restricciones hacen que la instancia se rechace antes de entregar los valores al código invitado.
Los valores canónicos de plugins.entries.<instance-key>.config del operador siguen siendo un mapa de cadenas marcado como secreto en memoria y se cifran al persistirse. El host deriva la clave de entrada versionada zpi1_… a partir de la identidad completa del paquete, la capacidad y el enlace; esto permite que distintos paquetes y ámbitos de capacidades reutilicen de forma segura alias como main. El manifiesto del paquete admitido selecciona el esquema. Un valor string se almacena directamente; los valores boolean, integer y number usan texto escalar de JSON como "true", "4" o "0.5"; los valores array y object usan texto JSON como '["urgent","ops"]' o '{"region":"us-east"}'. El host materializa y valida el conjunto completo de datos en JSON tipado para cada uso y, después, particiona cada propiedad exactamente una vez. Para una herramienta, los valores no secretos se inyectan bajo la clave reservada __config:
{
"prompt": "una puesta de sol",
"__config": {
"retry_limit": 4,
habilitado: true,
"etiquetas": ["urgente", "ops"]
}
}
La api_key omitida se lee explícitamente con secrets.get("api_key") si su esquema la marca como secreta. runtime.rs elimina cualquier __config proporcionado por el llamador antes de inyectar la sección pública, por lo que la sección no se puede falsificar. La inyección pública de la herramienta y las lecturas de secretos dentro de un mismo marco execute comparten una única revisión resuelta de la configuración activa; el marco se descarta tras un éxito, error, trampa, pánico o cancelación. La exportación configure de un canal no tiene ningún parámetro de configuración. Llama a config.get para obtener el objeto público y a secrets.get para las propiedades secretas, al igual que cada exportación operativa posterior que usa configuración. Ambas importaciones dentro de una misma llamada comparten una única revisión resuelta. El host descarta esa vista materializada tras un éxito, error, trampa, pánico o cancelación. Por lo tanto, los cambios en la configuración pública y las credenciales dentro de la misma vinculación lógica están disponibles conjuntamente en la siguiente operación cuando el huésped conforme resuelve ambos en el punto de uso.
Cuando el manifiesto solicita config_read, pero el host no lo concede efectivamente, la resolución sustituye el valor por un objeto vacío y valida ese objeto antes de que se ejecute el código invitado. Un esquema con campos obligatorios falla de forma segura durante la construcción. Si el objeto vacío es válido, las herramientas omiten el __config vacío, mientras que el canal config.get y secrets.get devuelven access-denied. Un complemento solo ve su propia sección.
Las exportaciones de metadatos estáticos del canal no pueden llamar a ninguno de los dos servicios de configuración y se leen una sola vez durante la carga. Por lo tanto, cambiar la identidad de un bot o una cuenta, o cualquier capacidad derivada de la configuración, identificador propio, mención o retardo entre mensajes, requiere reconstruir el ciclo de vida del canal; la configuración pública ordinaria y la rotación de credenciales para la misma vinculación lógica no lo requieren. Tool y channel son los consumidores actuales de la configuración. El mundo de memoria aún no tiene ninguna importación de configuración, por lo que los plugins de memoria no deben solicitar config_read hasta que se incorporen ese ABI y el cableado del runtime.
Host de componentes WASI
El host (crates/zeroclaw-plugins/src/component.rs) compila e instancia componentes contra un único wasmtime::Engine asíncrono. La forma en que se carga un archivo .wasm depende del backend de ejecución de la compilación:
plugins-wasm-cranelift: hay un backend JIT presente, así queload_componentcompila un componente.wasmal cargarlo medianteComponent::from_file.- Sin backend JIT (
plugins-wasm-pulleyo solo en tiempo de ejecución): no hay compilador en el binario, por lo queload_componentdeserializa el archivo directamente medianteComponent::deserialize_file, tratándolo como un.cwasmprecompilado producido por una wasmtime compatible. Un artefacto no coincidente es rechazado por la comprobación de versión de deserialize.
Ambas funciones del backend incorporan plugins-wasmtime; la ruta de carga se basa en si el compilador cranelift está en la compilación, no en pulley.
Límites de ejecución por llamada
Cada exportación del invitado se ejecuta con límites de recursos por llamada que el host aplica al store. El motor habilita la medición de combustible, y cada llamada recibe un presupuesto de combustible nuevo para que un componente descontrolado o malicioso provoque un trap en lugar de bloquear al host. El host también aplica un plazo de tiempo real al futuro completo de la exportación, incluido el tiempo de espera de importaciones asíncronas del host como wasi:http; las cesiones periódicas de combustible garantizan que el cálculo ininterrumpido del invitado no pueda impedir que se ejecute ese temporizador, y las importaciones del host accesibles desde el invitado nunca bloquean el ejecutor (los registros de log se entregan a una cola acotada y los escribe un hilo dedicado del host), por lo que el plazo sigue siendo observable mientras se ejecuta el trabajo del host. Un tope de StoreLimits limita la memoria lineal, los elementos de las tablas y el número de instancias. El mundo de herramientas obtiene un store nuevo en cada ejecución; los stores del canal persistente y de la memoria se reabastecen de combustible antes de cada llamada, de modo que un complemento de larga duración obtiene un presupuesto nuevo en lugar de agotarlo durante su vida útil.
Los cinco límites se pueden ajustar por el operador y todos los valores se validan para que no sean cero: plugins.limits.call_fuel (valor predeterminado: 1.000.000.000 unidades de instrucción), plugins.limits.call_timeout_ms (valor predeterminado: 30.000 milisegundos), plugins.limits.max_memory_mb (valor predeterminado: 256), plugins.limits.max_table_elements (valor predeterminado: 100.000) y plugins.limits.max_instances (valor predeterminado: 64). Un almacén solo puede crearse con límites explícitos, por lo que ninguna ruta de carga puede construir un plugin sin aislamiento. Las opciones de solicitud de wasi:http del invitado pueden finalizar una llamada antes, pero no pueden ampliar el plazo límite del host. Un almacén en caliente interrumpido nunca se reanuda: los canales lo recrean a partir de entradas propiedad del host en la siguiente llamada, mientras que las instancias de memoria siguen sin estar disponibles hasta que su propietario las reconstruye. Los campos canónicos y los valores predeterminados se encuentran en la referencia de Config.
espacio de direcciones de 32 bits (wasip2 es wasm32)
El target del plugin es wasm32-wasip2, y el motor host está compilado con la medición de fuel habilitada (Config::consume_fuel(true)) sin wasm_memory64. El límite del plugin es un formato fijo de 32 bits, y eso tiene consecuencias que conviene decir claramente:
- El espacio de direcciones del invitado es de 32 bits. Un plugin se ejecuta en una memoria lineal wasm32. Los valores grandes cruzan el límite por valor:
media-attachmentde un plugin de canal transporta sus bytes completos como unlist<u8>, ywit/v0/channel.witya señala que esto puede ser de varios megabytes y deja un modelo de identificador de recurso para una revisión futura. Dentro de ese espacio de 32 bits, el host aplica un límite explícito de memoria por almacén desdeplugins.limits.max_memory_mb(predeterminado 256), así que un invitado queda limitado por el menor entre el espacio de direcciones wasm32 y ese límite configurado por ZeroClaw. - La ABI de componentes reduce los desplazamientos a 32 bits independientemente del tamaño de palabra del host. Incluso en un host de 64 bits, los desplazamientos de lista y cadena en la ABI canónica son
i32.memory64amplía el direccionamiento de memoria lineal de un invitado, no la ABI canónica del modelo de componentes, por lo que habilitarlo no haría que los campos a nivel de WIT fueran de 64 bits. - No existe ningún objetivo wasip2 de 64 bits contra el que enlazar.
wasm32-wasip2es el único objetivo de WASI Preview 2 en rustc y LLVM hoy; un plugin no puede compilarse como un componente p2 de 64 bits, así que no hay nada que el host pueda cargar aunque el motor habilitaramemory64.
Esta es una restricción de la cadena de herramientas upstream, no una limitación del host que un flag en este repo pueda levantar. Cuando un target p2 de 64 bits y una ABI de componente más amplia lleguen upstream, la costura bindgen! se regenerará contra ellos y los anchos de campo se revisarán en el WIT dentro de la ventana wit/VERSIONING.md. Hasta entonces, trate el límite del plugin como de 32 bits por construcción.
Firmas
Los manifiestos de plugins pueden incluir una firma Ed25519 (crates/zeroclaw-plugins/src/signature.rs). La firma está codificada en base64url a partir de los bytes canónicos del manifiesto (el TOML analizado, tras eliminar únicamente las entradas raíz exactas signature y publisher_key); la clave pública del editor está codificada en hexadecimal. Las propiedades anidadas del esquema con esos nombres siguen estando firmadas. El host aplica uno de los tres modos definidos en plugins.security.signature_mode:
| Modo | Plugin sin firmar | Firma no confiable o no válida |
|---|---|---|
strict | rechazado | rechazado |
permissive | cargado con una advertencia | cargado con una advertencia |
disabled | cargado | no verificado |
La verificación se ejecuta tanto en discovery como en install. Discovery omite un plugin que no cumple su política en lugar de abortar todo el host; install devuelve el error.
Escribir un plugin en Rust
Un plugin es un crate cdylib que apunta al modelo de componentes. Genera los bindings de invitado a partir del mismo paquete wit/v0 que usa el host, implementa el world exportado y compila para wasm32-wasip2. Para ver los recorridos completos desde un crate vacío hasta un plugin instalado, consulta las guías de plugins; las notas a continuación cubren la compilación y la instalación.
Construyendo
sh
# Instala el destino de WASI Preview 2 (una vez)
rustup target add wasm32-wasip2
# Compilar el componente
cargo build --target wasm32-wasip2 --release
El componente de salida está en target/wasm32-wasip2/release/<crate_name>.wasm. Cópialo junto a tu manifest.toml. Para una compilación del host solo en tiempo de ejecución sin backend JIT, precompila el componente a un .cwasm con un wasmtime compatible y distribúyelo en su lugar, ya que ese tipo de host deserializa en vez de compilar al cargarse.
Las pruebas de complementos de herramientas del host no dependen de un artefacto publicado: crates/zeroclaw-plugins/tests/fixtures/tool-fixture es un componente incluido en el árbol que se compila a partir del código fuente durante las pruebas, y reference_plugin.rs y reference_plugin_e2e.rs lo ejecutan mediante el mismo PluginHost, config_schema y las mismas rutas de resolución de configuración que utiliza el demonio. Si no se puede compilar el fixture, esas pruebas fallan.
Instalando
sh
# Copiar al directorio del complemento
zeroclaw plugin install /path/to/my-plugin/
# O manualmente
cp -r my-plugin/ ~/.zeroclaw/plugins/my-plugin/
Configuración
Los valores del operador se introducen actualmente mediante un almacenamiento genérico de mapas de cadenas: edite [[plugins.entries]] en TOML o use zeroclaw config set después de que la instalación de una herramienta haya inicializado su entrada de vinculación predeterminada. zeroclaw plugin info <package> muestra la misma clave de herramienta para la migración y posteriores ediciones. Estas interfaces automáticas de impresión e inicialización solo están disponibles para herramientas. Una clave de canal depende de su alias configurado, que install e info no gestionan. La construcción que tiene en cuenta el alias y resuelve la configuración tipada de un canal a partir de dicho alias configurado se incorporó en #10146; la visualización automática y la inicialización de la clave del canal durante la instalación siguen siendo manuales hasta la ceremonia de concesión en #9584, por lo que un paquete exclusivo de canal todavía no puede completar esta migración solo mediante install e info. Los formularios basados en esquemas y la ayuda contextual para campos aún no están implementados. Las interfaces actuales son:
- La CLI gestiona el ciclo de vida de los plugins con
list,search,install,remove,infoymigrate.zeroclaw config setescribe valores sin procesar individuales del plugin; no interpreta el esquema del plugin. - zerocode puede editar la configuración estática del host de plugins de ZeroClaw, pero todavía no genera campos específicos para cada plugin a partir de
config_schema. - La puerta de enlace web es de solo lectura para los complementos:
GET /api/pluginsinforma de los complementos cargados y de si el sistema está habilitado. - El host valida
config_schemaal admitir el paquete y vuelve a validar/materializar los valores del operador antes de que el guest los use. - El esquema del manifiesto, para los autores de plugins, es el único contrato de tipos y validación en el límite del código invitado. Define allí cada clave compatible y cada restricción; no dupliques ese contrato en una estructura de configuración del tiempo de ejecución del host. El código invitado debe deserializar el JSON validado por el host en su estructura tipada nativa.
El esquema de configuración estática proporciona la ruta genérica de almacenamiento y marcado de secretos, no un editor dinámico por complemento. Los tipos de configuración de los complementos en crates/zeroclaw-config/src/schema.rs incluyen #[prefix = "plugins"], #[prefix = "plugins.entries"] y #[prefix = "plugins.security"], y la derivación Configurable convierte cada campo con prefijo en una ruta de configuración genérica. Los campos secretos (el mapa config de una entrada de complemento está marcado con #[secret]) se cifran en reposo mediante el .secret_key adyacente. Los campos canónicos, los valores predeterminados y los valores de signature_mode para la configuración del host se encuentran en la referencia de configuración; ese esquema es la fuente de verdad, mientras que cada manifiesto de complemento es la fuente de verdad para la estructura de su configuración privada.
Compilar funcionalidades
El host de complementos es una opción de activación en tiempo de compilación. Las características a nivel de binario en el Cargo.toml del espacio de trabajo seleccionan si los complementos se incluyen en la compilación en absoluto y qué backend de ejecución se distribuye:
plugins-wasmes el paraguas que incorpora el host de plugins y su integración en tiempo de ejecución al binario. Cada característica de backend que se describe a continuación lo implica, por lo que habilitar cualquier backend de ejecución (p. ej.,--features plugins-wasm-cranelift) siempre incluye el host de plugins y su superficie de CLI; una compilación exclusiva de backend no puede producir silenciosamente un binario sin el subcomandoplugin. El paraguas por sí solo equivale aplugins-wasm-runtime-only: sin JIT, por lo que solo se cargan componentes.cwasmprecompilados.plugins-wasm-runtime-onlyes el más pequeño y el más rápido de iniciar: no hay JIT, por lo que los componentes se deserializan desde un.cwasmprecompilado.plugins-wasm-craneliftañade el JIT de Cranelift, por lo que un componente.wasmse compila al cargarse.plugins-wasm-pulleyes el más portátil y admite compilación en objetivos que Cranelift no cubre.
Estos delegan en las características del crate zeroclaw-plugins (plugins-wasmtime, plugins-wasm-cranelift, plugins-wasm-pulley) que conectan wasmtime. La ruta de carga se basa en si el compilador Cranelift está en la compilación, como se describe en WASI Component Host. Lee los comentarios de las características en el Cargo.toml del espacio de trabajo para las descripciones autorizadas.