Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Escribir un plugin de canal

Un complemento de canal es una integración con una plataforma de mensajería: entrega las respuestas del agente a una plataforma y expone los mensajes de la plataforma al agente. Es el tipo de complemento más complejo, porque un canal tiene larga duración, mantiene estado e interactúa con el entorno de ejecución mediante una superficie de 27 funciones, de las cuales solo 5 son obligatorias.

Esta guía supone que has compilado el plugin de herramienta y entiendes la configuración del crate, la regla __config, el registro y la instalación. Está verificada frente a wit/v0/channel.wit y el adaptador del host en crates/zeroclaw-plugins/src/wasm_channel.rs.

Estado de integración. Los complementos de canal los construye un demonio en ejecución. Un paquete instalado asociado mediante [channels.plugin.<alias>] se incorpora al iniciarse y se supervisa exactamente igual que un canal nativo. Consulta Activar un complemento de canal más abajo.

El ciclo de vida

La forma en tiempo de ejecución de un complemento de canal difiere de la de una herramienta en tres formas fundamentales, y cada una impulsa una decisión de diseño en tu código:

  1. Un store persistente durante toda la vida útil del complemento. El host crea una instancia de tu componente una vez (WasmChannel::from_wasm) y mantiene el store protegido por un mutex asíncrono. El componente puede conservar el estado del protocolo propiedad del guest entre llamadas, pero la configuración del operador sigue siendo propiedad del host. Un complemento compatible debe llamar a config.get y secrets.get en cada operación que los necesite y no debe copiar sus resultados al estado persistente del guest. El host descarta su vista materializada después de cada llamada, pero no puede impedir que el código guest malicioso conserve el JSON o el texto sin formato devueltos. El store recibe combustible nuevo antes de cada llamada (call_channel! en component.rs), por lo que un canal de larga duración obtiene un presupuesto de combustible nuevo en cada llamada en lugar de agotarlo durante toda su vida útil.
  2. La configuración se solicita en el punto de uso. El host llama a su exportación configure sin argumentos exactamente una vez, durante la carga y antes de cualquier otra exportación. Llame a config.get para obtener el objeto JSON público tipado y validado conforme al config_schema de su manifiesto; las propiedades marcadas con x-secret = true se omiten y deben leerse mediante secrets.get. Las lecturas públicas y secretas en configure, o en cualquier exportación operativa posterior, comparten una única revisión resuelta de la configuración. Por tanto, en la siguiente operación se ven conjuntamente la configuración pública de la misma vinculación y la rotación de credenciales. Las llamadas realizadas durante la instanciación y el descubrimiento estático devuelven unavailable sin resolver la configuración. El descubrimiento estático incluye name, plugin-info, get-channel-capabilities, self-handle, self-addressed-mention y multi-message-delay-ms; cambiar la identidad del bot o de la cuenta, u otros metadatos estáticos, requiere reconstruir el ciclo de vida del canal.
  3. No escuchas; el host te alimenta. El contexto WASI no tiene capacidad de escucha de red. El tráfico entrante te llega a través de la interfaz importada inbound: el host ejecuta el listener real (servidor de webhook, túnel del proveedor, cliente de sondeo), encola cada mensaje recibido en una InboundQueue, y tu exportación poll-message la vacía llamando a inbound-poll. Haz una vaciado por lotes con inbound-pending si resulta útil.

Exportaciones requeridas

Cinco funciones no tienen un valor predeterminado de rasgo de Rust y deben funcionar realmente (world channel-plugin doc, channel.wit):

ExportarContrato
nameNombre de canal legible para humanos.
configureInicialización completa en tiempo de carga. No acepta argumentos; llama a config.get y secrets.get para una única revisión actual. Una cadena de error hace que falle la carga.
sendEntrega un send-message (content, recipient, optional subject/thread/attachments) a la plataforma.
poll-messageSin bloqueo: devuelve el siguiente mensaje entrante o none inmediatamente. Nunca bloquees; el puente de sondeo del host gestiona el ritmo.
get-channel-capabilitiesDevuelve la máscara de bits de los métodos opcionales que realmente implementas. Se llama una vez al cargar.

El puente de polling merece una nota: el host ejecuta un bucle de poll-to-push (listen en wasm_channel.rs) que llama a poll-message con retroceso exponencial de 50ms a 500ms mientras la cola está vacía, reiniciándose cuando hay tráfico. Si tu poll-message falla, el host marca el canal como poll-unhealthy, registra el evento y aplica retroceso; un plugin cuyo poll sigue fallando informa unhealthy a través de health_check incluso si no exporta ningún health-check propio. Por tanto, fallar en poll-message es visible, no fatal, pero hace que tu canal sea inútil. Mantenlo simple: vacía la cola, traduce, devuelve.

Marcas de capacidad: los 22 métodos opcionales

Todo lo demás en la interfaz está controlado por marcas de channel-capabilities. El patrón (idéntico al del mundo de memoria):

  • El host lee tus flags una vez al cargar.
  • Para cada bandera sin establecer, el host usa el valor predeterminado del trait de Rust y nunca llama a tu exportación.
  • Aún debes exportar todas las funciones; un stub que devuelve el valor predeterminado documentado compila y nunca se llama.

Los valores predeterminados de cada opción están documentados en línea en channel.wit, junto a la declaración de las opciones, que es la fuente de verdad. En resumen, los grupos:

GrupoBanderasLo que te da implementar
Saludhealth-checkInformar la accesibilidad de la plataforma; combinado con el estado de sondeo por el adaptador del host.
Identidadself-handle, self-addressed-mention, drop-self-messageProtección contra auto-bucle (el runtime descarta los propios mensajes del bot) y formas correctas de mención con @ en el prompt del sistema por canal. El host almacena en caché self-handle y self-addressed-mention al cargar; se leen una sola vez.
Escribiendostart-typing, stop-typingComponiendo indicadores mientras el agente piensa.
Borradoressupports-draft-updates, send-draft, update-draft, update-draft-progress, finalize-draft, cancel-draftEdición progresiva de mensajes: el runtime transmite la respuesta a un mensaje de plataforma editable en lugar de esperar a la finalización. Implemente los seis juntos o ninguno.
Streaming de múltiples mensajessupports-multi-message-streaming, multi-message-delay-msEntrega párrafo por párrafo con un retraso mínimo entre mensajes (predeterminado 800ms, almacenado en caché al cargar).
Moderaciónadd-reaction, remove-reaction, pin-message, unpin-message, redact-messageReacciones con emoji, fijación, eliminación de mensajes.
Interacciónrequest-approval, request-choice, supports-free-form-askPrompts de aprobación de llamadas a herramientas y preguntas de opción múltiple presentadas de forma nativa en la plataforma.

Empieza con los 5 obligatorios más health-check, y añade grupos a medida que la plataforma los admita. Publicar una bandera que no has implementado es peor que omitirla: el host llamará a tu exportación y confiará en la respuesta.

La superficie de aprobación

request-approval es el punto de integración más profundo. El runtime presenta una compacta approval-request (nombre de herramienta, resumen de argumentos, argumentos JSON sin procesar opcionales) y tu canal la representa como la plataforma lo permita (botones, reacciones, una convención de respuesta). La variante approval-response que devuelves impulsa la maquinaria de seguridad:

  • approve: ejecuta esta única llamada
  • deny: recházalo
  • always-approve: ejecuta y añade la herramienta a la lista de अनुमति permitidas con alcance de sesión
  • deny-with-edit(string): rechazar, pero proporcionar argumentos de reemplazo editados

Devuelve none cuando el prompt no pueda mostrarse; el llamador recurre a denegación automática. Fallar en cerrado.

Forma del mensaje entrante

Traduce los eventos de plataforma en registros inbound-message de forma fiel. La lógica de threading del runtime se basa en los campos del payload de la plataforma, mientras que la identidad de enrutamiento proviene únicamente del endpoint emitido por el host (channel.wit, from_wit_inbound en wasm_channel.rs):

  • id, sender, content: lo básico. reply-target es a donde debe ir una respuesta (ID del canal, ID del chat, dirección de correo electrónico).
  • channel y channel-alias son indicaciones heredadas que se conservan en el registro v0. El host ignora ambas para el enrutamiento y sella el tipo de canal admitido y el enlace configurado, por lo que un complemento no puede seleccionar otro propietario o sesión.
  • thread-ts lleva el identificador de hilo de la plataforma para las respuestas encadenadas; subject existe para el encadenamiento de correo electrónico.
  • interruption-scope-id agrupa mensajes para interrupción/cancelación. Déjalo en none para mensajes de nivel superior.
  • attachments transportan bytes brutos completos a través del límite (media-attachment: nombre de archivo, bytes, tipo MIME opcional). Una nota de voz son varios megabytes que cruzan por valor; este es el coste documentado del límite de 32 bits, y un modelo de manejadores de recursos queda explícitamente aplazado para una futura revisión de WIT.

En el lado de salida, send-message refleja los mismos campos; el token de cancelación de SendMessage de Rust se omite deliberadamente del registro WIT porque es un concepto del lado del host que no tiene significado dentro del plugin.

Esqueleto

La estructura, omitiendo la traducción por plataforma que es tu trabajo real:

#![allow(unused)]
fn main() {
#[cfg(target_family = "wasm")]
mod component {
    wit_bindgen::generate!({
        path: "wit/v0",
        world: "channel-plugin",
        features: [plugins-wit-v0],
    });

    use exports::zeroclaw::plugin::channel::{
        ApprovalRequest, ApprovalResponse, ChannelCapabilities,
        Guest as Channel, InboundMessage, SendMessage,
    };
    use exports::zeroclaw::plugin::plugin_info::Guest as PluginInfo;
    use zeroclaw::plugin::config::get as config_get;
    use zeroclaw::plugin::inbound::inbound_poll;
    use zeroclaw::plugin::secrets::get as secret_get;

    #[derive(serde::Deserialize)]
    #[serde(deny_unknown_fields)]
    struct ChannelConfig {
        api_base: String,
    }

    fn current_config() -> Result<ChannelConfig, String> {
        let json = config_get().map_err(|_| "la configuración pública no está disponible".to_string())?;
        serde_json::from_str(&json).map_err(|e| format!("JSON de configuración no válido: {e}"))
    }

    fn current_api_token() -> Result<String, String> {
        secret_get("api_token").map_err(|_| api_token no está disponible.to_string())
    }

    fn current_inputs() -> Result<(ChannelConfig, String), String> {
        // Ambas importaciones de esta exportación comparten una revisión canónica resuelta.
        Ok((current_config()?, current_api_token()?))
    }

    struct MyChannel;

    impl Channel for MyChannel {
        fn name() -> String {
            "my-platform".to_string()
        }

        fn configure() -> Result<(), String> {
            let (config, api_token) = current_inputs()?;
            validate_configuration(&config.api_base, &api_token)
        }

        fn send(message: SendMessage) -> Result<(), String> {
            let (config, api_token) = current_inputs()?;
            // Entrega saliente de la plataforma mediante wasi:http
            // (requiere el permiso http_client en el manifiesto). Construye la
            // solicitud a partir de los valores de esta llamada; nunca conserves una segunda copia.
            send_to_platform(&config.api_base, &api_token, message)
        }

        fn poll_message() -> Option<InboundMessage> {
            // Vacía la cola alimentada por el host y traduce.
            inbound_poll().map(translate_inbound)
        }

        fn get_channel_capabilities() -> ChannelCapabilities {
            ChannelCapabilities::HEALTH_CHECK
        }

        fn health_check() -> bool {
            current_inputs().is_ok()
        }

        // Cada otro método: un stub que devuelve el valor predeterminado documentado por WIT.
        // El host nunca los llama mientras su bandera no esté establecida.
        // ...
    }

    export!(MyChannel);
}
}

current_inputs se llama deliberadamente en el punto de uso. El host vincula ambas importaciones a este paquete admitido, la capacidad channel y el alias; las lecturas de una exportación comparten una misma revisión de configuración resuelta, mientras que la siguiente exportación puede observar una configuración pública del mismo vínculo junto con la rotación de credenciales. ChannelConfig es una vista tipada por llamada y se descarta junto con el token. No añadas una caché de configuración o credenciales thread_local.

Manifiesto y permisos

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:

CampoObligatorioSignificado
nameSometimes 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.
versionSometimes 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.
descriptionnoDescripción legible para humanos mostrada por zeroclaw plugin list.
authornoNombre del autor u organización.
wasm_pathpara las capacidades de WASMNombre 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.
capabilitiessí, no vacíoQué es el plugin: cualquiera de tool, channel, memory, observer, skill (PluginCapability, serializado en snake_case).
permissionsnoServicios 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_schemaexactamente con config_readBorrador 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.
signaturenoFirma Ed25519 en Base64url sobre los bytes canónicos del manifiesto. Se establece al firmar para distribución.
publisher_keynoClave 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.

Para un canal: capabilities que contiene channel y, casi con toda seguridad, tanto config_read (ninguna plataforma funciona sin credenciales) como http_client. El adaptador de canal implementa wasi:http saliente, pero solo lo enlaza después de que se valida esa concesión; sin ambas piezas, send no tiene ruta de red hacia la plataforma.

Asocia config_read con el esquema que consume ChannelConfig:

name = "my-platform"
version = "0.1.0"
wasm_path = "my_platform.wasm"
capabilities = ["channel"]
permissions = ["config_read", "http_client"]

[config_schema]
"$schema" = "https://json-schema.org/draft/2020-12/schema"
type = "object"
additionalProperties = false
required = ["api_base", "api_token"]

[config_schema.properties.api_base]
type = "string"
minLength = 1

[config_schema.properties.api_token]
type = "string"
minLength = 1
x-secret = true

El host valida ambas propiedades como un único objeto. config.get devuelve JSON tipado que contiene api_base y omite api_token, disponible únicamente mediante secrets.get. Como ambas son obligatorias, si no se concede config_read, la operación falla de forma segura antes de que se ejecute el código invitado, en lugar de iniciar un canal sin la configuración necesaria. Cada instancia de canal selecciona la clave plugins.entries derivada de su paquete completo, de la capacidad channel y de la identidad de vinculación, al tiempo que reutiliza este único esquema propio del paquete. Por tanto, los alias idénticos de distintos paquetes permanecen aislados. Los comandos install e info no pueden crear esta clave porque no son propietarios del alias de canal configurado; su comportamiento automático de impresión e inicialización es exclusivo de la herramienta, por lo que la entrada de una instancia de canal se escribe manualmente.

Llame a config.get y secrets.get dentro de cada operación que los use. El host resuelve como máximo una revisión canónica para esa llamada y descarta su vista después. Una configuración pública y una rotación de credenciales dentro de la misma vinculación lógica se hacen visibles conjuntamente en la siguiente operación, sin recargar el demonio ni reconstruir el canal. Cambiar la identidad del bot o de la cuenta, las capacidades anunciadas, el identificador propio, la mención u otros metadatos de carga requiere reconstruir el ciclo de vida del canal, porque esas exportaciones se leen una sola vez durante el descubrimiento estático.

Para un esquema opcional cuyo objeto vacío es válido, una instancia a la que se ha denegado la concesión efectiva de config_read puede cargarse, pero config.get y secrets.get devuelven access-denied. Cualquiera de las dos importaciones devuelve unavailable durante la instanciación o el descubrimiento estático, después de un fallo del resolvedor o de la validación, o cuando se agota el presupuesto compartido de llamadas al host. secrets.get también devuelve not-found para un nombre ausente o que no tiene marcado x-secret = true.

Activación de un complemento de canal

Un paquete instalado no hace nada hasta que un operador lo vincula a una instancia de canal lógico. La vinculación nombra el paquete y nada más; el alias es la identidad de la instancia:

[plugins]
enabled = true

[channels.plugin.operations]
package = "acme.chat"
enabled = true

[agents.support]
channels = ["plugin.operations"]

El alias se convierte en una referencia de canal ordinaria, por lo que plugin.operations se enruta, supervisa, reinicia y direcciona exactamente igual que telegram.main. Dos alias pueden nombrar un mismo paquete; cada uno obtiene su propia instancia, su propio almacén y su propia clave plugins.entries, por lo que no comparten ningún estado.

Una instancia solo se admite cuando se cumplen todas las condiciones siguientes. Cada una es una barrera deliberada de cierre ante fallos, y una declaración que no cumple una de ellas queda inerte en lugar de iniciarse parcialmente:

  • plugins.enabled es true.
  • El enabled de la declaración es true.
  • El paquete especificado está instalado y su manifiesto declara la capacidad channel.
  • Algunas listas de agentes habilitadas incluyen plugin.<alias> en sus channels. Un enlace sin referencias ejecutaría un listener sin ningún destino al que entregar.

La admisión tiene lugar antes de que se ejecute cualquier código invitado: se decide a partir de manifiestos que el host del paquete ya ha verificado, por lo que un paquete cuyo componente está dañado se planifica y se rechaza exactamente igual que uno que está intacto. Un paquete que supera la admisión pero después no se puede construir se registra y se omite, de modo que un complemento defectuoso no puede impedir que el demonio inicie los demás canales.

plugins.max_active_instances limita cuántas instancias lógicas se admiten entre todas las capacidades. Las vinculaciones explícitas de canales tienen prioridad sobre las herramientas y habilidades detectadas automáticamente, por lo que un directorio de plugins lleno no puede desplazar un canal que el operador haya configurado manualmente.

El mismo conjunto admitido rige los tres cargadores: el cargador de canales, el registro de herramientas y el cargador de habilidades de plugins. Por lo tanto, el límite máximo es un único presupuesto compartido, no uno por capacidad. Un paquete que proporciona tanto un canal como una herramienta consume realmente dos ranuras, y una herramienta o habilidad que supera el límite ni siquiera se construye. La admisión es una función pura de tu configuración actual y de los paquetes instalados: no mantiene ningún contador, por lo que los registros de herramientas que se reconstruyen por agente, por ejecución de CLI, por delegado y por ejecución de SOP vuelven a derivar el mismo conjunto cada vez, en lugar de agotar el límite durante la vida útil de un demonio de ejecución prolongada.

Las instancias de herramientas y habilidades se detectan automáticamente, por lo que solo se admiten cuando plugins.auto_discover es true. Las declaraciones explícitas de [channels.plugin.<alias>] no lo necesitan. Con plugins.enabled = true y auto_discover = false, obtienes exactamente las vinculaciones de canales que declaraste y nada más.

Migración desde plugins.max_plugins. La clave anterior nunca se aplicó y se ha reemplazado por plugins.max_active_instances. Las dos cuentan cosas distintas: la clave anterior contaba los paquetes instalados, mientras que la nueva cuenta las instancias lógicas admitidas, por lo que un paquete que proporciona tanto un canal como una herramienta consume dos. Dado que las unidades difieren, un valor existente de max_plugins no se transfiere: se ignora y la nueva clave usa su valor predeterminado. Establece max_active_instances explícitamente si dependías de un límite distinto del predeterminado.

Qué aún no está conectado

Los canales de Plugin se construyen de forma asíncrona, después de que ya se hayan creado las interfaces síncronas del mapa de canales. Por lo tanto, las herramientas direccionadas por canal todavía no pueden apuntar a un canal de Plugin. El sondeo entrante y la entrega saliente a través del listener supervisado no se ven afectados; solo falta el direccionamiento desde las herramientas.

Compilar e instalar

Instala el objetivo WASI Preview 2 una vez y, luego, compila el componente:

rustup target add wasm32-wasip2
cargo build --release --target wasm32-wasip2

El componente se encuentra en target/wasm32-wasip2/release/<crate_name>.wasm (los guiones en el nombre del crate se convierten en guiones bajos). Cámbiale el nombre a lo que declare tu wasm_path en el manifiesto cuando ensambles el directorio del plugin.

[!IMPORTANTE] Los archivos .wasm y .cwasm compilados son artefactos binarios, a menudo de varios megabytes cada uno. No los incluyas en un árbol de código fuente de git sin Git LFS: cada recompilación confirmada como un blob plano infla el historial del repositorio de forma permanente, y las herramientas de git diff/revisión no pueden con ellos. Trátalos como cualquier otro resultado de compilación: añade target/ y *.wasm/*.cwasm a .gitignore, y distribúyelos mediante un artefacto de lanzamiento o un archivo del registro de plugins en su lugar. Si un artefacto realmente debe vivir en el árbol, registra el patrón con LFS (git lfs track "*.wasm") antes del primer commit.

Si el host de destino es una compilación solo de ejecución (sin backend JIT compilado), no puede compilar .wasm al cargar; en su lugar, deserializa un .cwasm precompilado. Precompílalo con una CLI de wasmtime cuya versión coincida con la del host y distribuye el .cwasm como el artefacto wasm_path. Un artefacto con versión no coincidente es rechazado por la comprobación de deserialización de wasmtime, no se carga incorrectamente de forma silenciosa.

Estos comandos requieren un binario compilado con el host de plugins integrado. Los binarios de lanzamiento precompilados que distribuye el instalador se compilan sin la característica plugins-wasm, por lo que zeroclaw plugin ... es un subcomando no reconocido y los plugins instalados nunca se descubren. Compila desde el código fuente con un backend de ejecución de plugins, p. ej. cargo build --release --features plugins-wasm-cranelift.

Cada complemento vive en su propio subdirectorio del directorio de plugins (predeterminado ~/.zeroclaw/plugins/, resuelto mediante plugins.plugins_dir), que contiene el manifiesto y el componente nombrado para coincidir con wasm_path:

~/.zeroclaw/plugins/
└── my-plugin/
    ├── manifest.toml
    └── my-plugin.wasm

Instala desde un directorio local (esto valida la forma del manifiesto y ejecuta la directiva de firmas antes de copiar nada):

zeroclaw plugin install ./my-plugin/

Habilita el sistema de plugins y confirma la detección:

zeroclaw config set plugins.enabled true
zeroclaw plugin list
zeroclaw plugin info my-plugin

zeroclaw plugin list y zeroclaw plugin info confirman que un paquete está instalado y se puede descubrir, pero el descubrimiento no implica la activación. plugins.enabled = true activa el host de plugins; las capacidades de herramientas y habilidades descubiertas automáticamente se cargan en tiempo de ejecución solo cuando plugins.auto_discover = true también está habilitado, y ese indicador es false de forma predeterminada (cierre ante fallos):

zeroclaw config set plugins.auto_discover true

Por sí solo, plugins.enabled = true te proporciona los canales que declares en [channels.plugin.<alias>] y ninguna herramienta ni habilidad de plugin: un paquete de herramientas o habilidades puede aparecer en zeroclaw plugin list y, aun así, no aportar nada en tiempo de ejecución. Las vinculaciones explícitas de canales reciben su nombre del operador en lugar de descubrirse automáticamente, por lo que no necesitan auto_discover; la opción controla únicamente las herramientas y habilidades descubiertas automáticamente.

Se omitió en el descubrimiento un plugin que faltaba en zeroclaw plugin list: consulta el registro de inicio para ver la advertencia de omisión (manifiesto mal formado, archivo wasm_path faltante o rechazo por la política de firmas).

Probando contra el contrato del host

Las pruebas del adaptador del host y del resolvedor de configuración son la especificación ejecutable: cubren la materialización tipada y la validación del esquema, el ámbito público y secreto en el punto de uso, la rotación coherente en la misma revisión, las concesiones denegadas, la denegación del descubrimiento estático, la transferencia a la cola entrante, el despacho condicionado por capacidades y el seguimiento del estado de los sondeos.

Para ejecutar tu propio componente bajo esas semánticas exactas, escribe una prueba de integración que lo instancie a través del adaptador de host real. zeroclaw-plugins no está publicado en crates.io, así que recupéralo como una dependencia de desarrollo de git fijada al tag que coincida con tu host objetivo:

cargo add --dev zeroclaw-plugins \
  --git https://github.com/zeroclaw-labs/zeroclaw --tag <host-version> \
  --no-default-features --features plugins-wasm-cranelift

La prueba envuelve un PluginConfigResolver::new respaldado por el manifiesto y los valores del operador de prueba en PluginHostServices, carga tu componente mediante WasmChannel::from_wasm, coloca mensajes en la cola mediante el controlador de InboundQueue que expone y comprueba que tu poll-message consume y traduce el mensaje. Esa es la misma ruta de código que ejecutará un demonio de producción; superarla es la señal más sólida previa a la distribución que puedes obtener sin un host activo.

Siguiente