Escribir complementos
Un complemento es un módulo ES cuya exportación por defecto es un objeto Plugin. El contrato está en @alisio/sdk: tipos más dos utilidades (definePlugin, textResult), sin dependencias de runtime ni SDKs de proveedores.
import { definePlugin, textResult } from "@alisio/sdk";
export default definePlugin({
id: "acme.hello",
name: "Acme Hello",
description: "Adds a friendly greeting tool",
version: "0.1.0",
apiVersion: 1,
setup(api) {
api.tools.register({
name: "hello",
description: "Greets the user.",
effect: "read",
inputSchema: { type: "object", properties: {}, additionalProperties: false },
async execute() {
return textResult("Hello!");
},
});
},
});Los complementos son código de confianza
Los complementos se ejecutan en el mismo proceso con todos los privilegios del proceso de Alisio. Ni un manifiesto de complemento ni un subproceso son un sandbox, y el campo effect no aísla nada. Carga solo código en el que confíes.
El objeto Plugin
| Miembro | Descripción |
|---|---|
id | ID único que cumple ^[a-z0-9][a-z0-9.-]{0,63}$, por ejemplo acme.hello |
version | SemVer, por ejemplo 0.1.0 o 0.1.0-beta.1 |
apiVersion | Siempre 1 |
name, description | Texto opcional y legible para el catálogo de /plugins |
categories | Categorías de catálogo opcionales (consulta la sección siguiente, Categorías de complementos). El host también deriva model-provider de los registros de proveedores |
setup(api) | Registra todo; puede ser asíncrono. Si falla, se revierten los registros parciales |
extensions | Opcional; proveedores declarativos para puntos de extensión, registrados con prioridad 0 |
dispose() | Opcional; libera recursos cuando Alisio se cierra. Se ejecuta solo al cerrar, nunca al deshabilitar un complemento ni con /reload, y lo limita pluginHooks.disposeTimeoutMs (consulta Cierre) |
definePlugin es una función de identidad que solo añade tipado. Se rechazan IDs de complemento duplicados.
Categorías de complementos
categories es una lista de agrupaciones de catálogo. La TUI agrupa las filas de /plugins por la primera categoría declarada y usa "General" cuando el complemento no declara ninguna; la vista de detalle lista todas las categorías. Los autores de complementos pueden declarar cualquier valor aceptado; el host solo deriva model-provider por su cuenta, a partir de los registros de proveedores. Las categorías describen qué hace el complemento, así que un mismo complemento puede declarar varias.
| Categoría | Significado | Usada por los integrados |
|---|---|---|
model-provider | Registra proveedores de modelos | openai-compatible |
methodology-harness | Agrupa un flujo de trabajo de metodología de desarrollo | — |
memory | Memoria persistente, recuerdo y resúmenes de sesión | memory |
subagents | Delegación, sesiones hijas y gestión de agentes | subagents |
search | Proveedores de búsqueda web o vectorial | — |
tools | Colecciones de herramientas de propósito general | — |
security | Herramientas de auditoría, sandbox o permisos | — |
analytics | Instrumentación de uso/métricas (estadísticas de sesión, costo) | — |
mcp | Gestión de servidores MCP o utilidades para empaquetarlos | — |
storage | Backends de almacenamiento durable más allá del SQLite por defecto | — |
ui | Proveedores de presentación TUI (pantallas de inicio, mascotas, paneles) | — |
decisions | Proveedores de Decision Intelligence (api.decisions.registerProvider) | — |
El único proveedor de modelos integrado es openai-compatible. Los proveedores dedicados (DeepSeek, OpenCode Console (Zen), OpenCode Go) son complementos independientes publicados desde el monorepo alisio-plugins e instalables con alisio install npm:@alisio/plugin-{deepseek,opencode,opencode-go}; una vez instalados se registran como complementos model-provider y aparecen en /connect y /model.
Gestionar complementos en la TUI
Ejecuta /plugins para abrir el catálogo filtrable. [x], [ ], [!] y [*] significan activo, inactivo, fallido y reinicio necesario; las filas también distinguen los complementos integrados de etiquetas de origen externas seguras. Selecciona una fila para ver la descripción completa, la categoría y la acción disponible.
Los cambios actualizan atómicamente .alisio/config.json del proyecto actual y conservan los campos JSON no relacionados. Se aplican deliberadamente al iniciar Alisio de nuevo: los registros, hooks, almacenamiento y clientes de proveedor vivos no se eliminan parcialmente. Devolver un cambio al estado original del runtime elimina restart-required. Los complementos externos requieren un proyecto de confianza y confirmación explícita porque se ejecutan con todos los privilegios del proceso. Alisio impide desactivar un complemento de proveedor mientras lo retenga el proveedor activo o cualquier sesión enrutada viva, y también bloquea complementos con recursos de la sesión actual. Cambia de proveedor y cierra las sesiones retenidas, o reinicia Alisio, primero. Los perfiles y credenciales siguen siendo ajustes globales y nunca se copian a la configuración de complementos.
Referencia de PluginAPI
Cada método register/on devuelve una función para anular el registro. Todo lo que registra un complemento se elimina automáticamente cuando se descarga.
| Miembro | Descripción |
|---|---|
tools.register(tool) | Registra una herramienta (ToolDefinition) |
commands.register(name, handler, options?) | Registra un comando; handler(args: string, context?: { sessionId? }) => Promise<string> devuelve el texto que se muestra al usuario (sessionId es la sesión actual de la interfaz interactiva, cuando se conoce). options: { description?, argumentHint? }, mostrados en /help y en el autocompletado |
events.on(handler) | Observa los eventos versionados de ejecución (RunEvent: schemaVersion, runId, sessionId, seq, type, timestamp, data, más los opcionales eventId y correlationId). KnownRunEvent tipa el data de cada evento que emite el núcleo |
context.register(provider) | () => Promise<string>; añade texto al contexto del modelo |
resources.skills(path) | Añade una raíz de Agent Skills (relativa al archivo del complemento) |
resources.agents(path) | Añade un directorio de definiciones de agentes para complementos de delegación (consulta Subagentes) |
resources.list(kind) | Directorios registrados por todos los complementos para skills, prompts o agents, con el ID del complemento |
resources.prompts(path) | Añade un directorio de plantillas de prompts (*.md, relativo al archivo del complemento) |
state.get(key) / state.set(key, value) | Estado JSON pequeño por complemento, persistido en la base de datos de sesiones |
storage.sqlite(path) | Abre un archivo SQLite privado (0600), creando los directorios padre (0700). Devuelve el puerto de almacenamiento SqlDatabase |
views.register(view) | Registra una vista de datos con nombre y de solo lectura que los hosts, como la interfaz web, pueden leer. Ausente en un núcleo anterior: usa api.views?.register(...) |
apps.register(app) | Registra una aplicación de complemento de página completa junto al panel de proyectos. Ausente en un núcleo anterior: usa api.apps?.register(...) |
proof.registerVerifier(verifier) | Registra un verificador de Proof ({ id, describe, run(input, ctx) }) al que se enlazan los criterios como <pluginId>:<id>. Ausente en un núcleo anterior a Proof: usa api.proof?.registerVerifier(...) |
decisions.registerProvider(provider) | Registra un proveedor de Decision Intelligence (no lo activa); también available(), activeProvider(), tryDecide(request, options?) y el opcional activate(providerId, options?) (options.recommend preselecciona Yes tras una acción explícita del usuario). Ausente en un núcleo anterior: usa api.decisions?.registerProvider(...) |
paths | { state, config, cache }: directorios por complemento resueltos por el host y creados con modo 0700 en la primera lectura. Ausente en un núcleo anterior |
options | pluginOverrides[id].options de solo lectura (o {}), congelado cuando se ejecuta setup. Ausente en un núcleo anterior |
compaction.register({ beforeCompact, afterCompact }) | Hooks de compactación, ver más abajo |
session.onStart(handler) | El texto devuelto se inyecta una vez al comienzo de una sesión nueva y vacía (persistido en la sesión) |
session.onEnd(handler) | Se llama cuando termina una sesión interactiva (/clear, /exit, salida) |
model.complete(request) | Completado de texto agnóstico del proveedor: { system, messages, maxTokens?, model?, signal? } → Promise<string> |
models.list() / models.resolve(reference) | Lista modelos configurados sin credenciales y resuelve proveedor/modelo o un ID único sin proveedor |
providers.register(provider) | Añade metadatos, campos de configuración/autenticación, capacidades y una factoría. Los registros coexisten; /connect selecciona uno |
extensions.register(point, provider, options?) | Proporciona una implementación para un punto de extensión (mascot, startup-screen); options: { priority? } |
ui.status(key, text, detail?) | Texto breve en la barra de estado de la TUI; detail aparece en /stats; text: undefined lo elimina |
ui.panel(id, provider) | Un panel en árbol plegable (PanelProvider), solo en interfaces interactivas |
ui.select(request) | Pide al usuario que elija (SelectRequest: title, options con value, label, description?); resuelve undefined sin interfaz interactiva |
ui.open(sessionId) | Abre una sesión en una vista de solo lectura; devuelve false sin interfaz interactiva |
ui.interactive() | true cuando una interfaz interactiva puede responder a select |
sessions.* | Sesiones hijas, ver más abajo |
Los complementos de proveedores dependen solo de @alisio/sdk. Su factoría recibe por separado el profile sin secretos, las credentials secretas y valores heredados opcionales. El núcleo controla la persistencia y la activación. La limpieza del registro participa en el rollback del complemento; si falla la creación de un proveedor, el proveedor activo permanece intacto.
Herramientas
interface ToolDefinition {
name: string;
description: string;
inputSchema: JsonSchema;
effect?: "read" | "write" | "process" | "external" | "internal";
paths?: (input: Record<string, unknown>) => string[];
concurrent?: boolean;
execute(input: Record<string, unknown>, context: ToolContext): Promise<ToolResult>;
}concurrent: true permite que una llamada se ejecute a la vez que otras llamadas de lectura o concurrentes del mismo turno (las herramientas de delegación lo usan). ToolContext proporciona signal, workspace, emit(data) y session. Devuelve textResult(text, isError?). El efecto controla los permisos (Herramientas y permisos): las operaciones desconocidas o de complementos usan external por defecto; declara read solo para herramientas sin efectos secundarios.
Resultados enriquecidos (bloques ui)
Un resultado de herramienta puede añadir partes { type: "ui", block } junto a su texto. Un UiBlock son solo datos; los complementos nunca envían código de renderizado. Conserva siempre también una parte de texto: los proveedores, la compactación y la salida headless solo ven la proyección de texto, y el bloque es una indicación de presentación.
return {
content: [
{ type: "text", text: "2 passed, 1 failed" },
{
type: "ui",
block: {
kind: "test-results",
framework: "vitest",
suites: [{ name: "math", cases: [{ name: "adds", status: "passed" }] }],
},
},
],
};| Kind | Campos | Renderizado en la TUI |
|---|---|---|
table | columns, rows, caption? | Columnas alineadas |
key-value | entries, caption? | Dos columnas |
tree | nodes (label, children?, meta?) | Glifos de rama |
code | code, lang?, caption? | Código resaltado |
markdown | text | Markdown |
diff | patch? o before?/after?, path?, lang?, caption? | Parche unificado como código diff; si no, before/after etiquetados |
terminal | output, command?, cwd?, exitCode?, durationMs?, truncated? | Salida sin ANSI más exit <código> · <tiempo> |
mermaid | source, title? | Fuente como código mermaid |
math | latex, display? | LaTeX literal |
json | value, collapsedDepth?, caption? | JSON indentado, truncado |
test-results | suites (name, file?, cases), framework?, durationMs? | Tabla de suite, caso, estado y tiempo, más los fallos |
progress | steps (label, status, detail?), title? | Una línea por paso con un glifo de estado |
UI_BLOCK_KINDS lista todos los kinds. Acota los tamaños (unos 200 KB para un diff y 256 KB para la salida de terminal o el JSON serializado). Un kind que la versión de Alisio en ejecución no conoce, o un bloque mal formado, se muestra como JSON etiquetado en lugar de fallar, de modo que versiones anteriores pueden reproducir transcripciones más nuevas.
Reglas de nombres y prefijos
| Elemento | Complemento externo | Complemento integrado |
|---|---|---|
| Nombre de herramienta | p_<hash>_<name> (10 caracteres hexadecimales del SHA-256 del ID del complemento), para evitar colisiones y cumplir los límites de nombres del proveedor | Sin prefijo |
| Comando | <plugin id>:<name>, invocado como /command acme.hello:name args o /acme.hello:name; también desde alisio run "/acme.hello:name args", que imprime el resultado sin llamar al modelo (--json imprime una línea command_result), y desde la paleta web | Sin prefijo (por ejemplo /memory) |
Efecto internal | Se degrada a external | Permitido |
Hooks de compactación
export default definePlugin({
id: "my.notes",
version: "0.1.0",
apiVersion: 1,
setup(api) {
api.compaction.register({
async beforeCompact() {
return { outputFields: { notes: "array of short strings worth keeping" } };
},
async afterCompact({ extracted }) {
const notes = Array.isArray(extracted.notes) ? extracted.notes : [];
return { report: { summary: `notes: ${notes.length}` } };
},
});
},
});beforeCompact(input)recibesessionId,reason(manualoauto), losmessagesque van a reemplazarse,focusysignal. Puede devolverinstructions(añadidas a las instrucciones del resumidor) youtputFields(campos JSON adicionales de primer nivel, nombre → descripción, solicitados en la misma llamada al resumidor).afterCompact(result)recibe ademásmodel,replaced,structured,checkpoint,checkpointTextyextracted(los campos de este complemento, sin validar: valídalos tú mismo). Puede devolverinjectContext(texto añadido tras el checkpoint; mantenlo dentro de un presupuesto) yreport(summaryse muestra literalmente).
Hooks de sesión y completados del modelo
api.session.onStart(async (info) => `Project notes for ${info.workspace}`);
api.session.onEnd(async (info) => {
const summary = await api.model.complete({
system: "Summarize the session in one sentence.",
messages: [{ role: "user", text: `${info.messages.length} messages` }],
maxTokens: 200,
signal: info.signal,
});
api.state.set("lastSummary", summary);
});SessionInfo contiene sessionId, model, workspace, reason (start, clear o exit), messages y signal. model.complete usa el proveedor configurado (el modelo de la sesión cuando el complemento lo pasa) y no descuenta del presupuesto limits.maxTokens.
Puerto de almacenamiento
const db = api.storage.sqlite("/path/to/notes.sqlite");
db.exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, text TEXT)");
db.transaction(() => db.prepare("INSERT INTO notes (text) VALUES (?)").run("hello"));
const rows = db.prepare("SELECT * FROM notes").all();SqlDatabase es síncrono: exec(sql), prepare(sql) (las sentencias se cachean por texto SQL y ofrecen run, get, all), transaction(fn) (transacción inmediata; las llamadas anidadas se unen a la exterior) y close(). FTS5 está disponible. El host proporciona el driver, por lo que los complementos nunca dependen de un runtime concreto.
Vistas de datos
Una vista de datos es una función con nombre que devuelve JSON para una sesión y que los hosts leen sin ejecutar una herramienta. alisio serve las expone a la interfaz web (el complemento de memoria integrado alimenta así la pestaña Memoria).
import { definePlugin } from "@alisio/sdk";
export default definePlugin({
id: "notes",
version: "1.0.0",
apiVersion: 1,
setup(api) {
// `views` is absent on an older core: feature-detect it and keep working without it.
api.views?.register({
id: "recent",
description: "Recent notes of the session",
params: {
type: "object",
properties: { limit: { type: "integer", minimum: 1, maximum: 50, default: 20 } },
},
// `params` is already validated and coerced; `loadNotes` is your own storage code.
handler: (params, { sessionId }) => ({ items: loadNotes(sessionId, Number(params.limit)) }),
});
},
});idson letras minúsculas, dígitos y guiones (hasta 40 caracteres) y es único dentro del complemento;descriptiones obligatoria.paramses un objeto JSON Schema de primitivos (string,integer,number,boolean; sin$ref) cuyas claves desconocidas se rechazan. Las cadenas de consulta se convierten a los tipos declarados y se aplican los valores por defecto antes de ejecutarhandler(params, { sessionId, workspace, signal }).ViewParamsErrorconvierte un parámetro que el esquema no puede expresar en un400.- Por HTTP una vista es
GET /api/sessions/:sid/views/:plugin/:view?<params>, con la misma cookie de sesión y las mismas reglas de Host y Origin que el resto de rutas. Solo responden los complementos habilitados, la sesión debe existir y su workspace también, y una vista de otro complemento o de uno deshabilitado es un404. Se aceptan como máximo 16 parámetros de consulta de 512 caracteres. - La respuesta es JSON de como máximo 1 MiB (
view_too_large, 502) y el handler tiene 5 segundos (view_timeout, 504;signalaborta). Cualquier otro fallo es unview_failedgenérico (502): su mensaje y los parámetros nunca se envían ni se registran, porque pueden contener datos del proyecto. - Las vistas son de solo lectura por contrato, no por aislamiento. El host solo controla el método, los parámetros validados, el tiempo y el tamaño; no puede impedir que el código de un complemento escriba, y un handler que bloquee el bucle de eventos no se interrumpe con el timeout. Un complemento no es un sandbox.
Aplicaciones de complementos
Un complemento puede aportar una aplicación de página completa: una herramienta pequeña o una interfaz completa de varias páginas que vive junto al panel de proyectos y es independiente del chat. Regístrala durante setup; el host la elimina cuando el complemento se cierra o cuando setup falla.
import { definePlugin } from "@alisio/sdk";
export default definePlugin({
id: "acme.notes",
version: "1.0.0",
apiVersion: 1,
setup(api) {
// `apps` no existe en un núcleo anterior: avise en lugar de fallar en silencio.
if (!api.apps) throw new Error("This plugin needs an Alisio host with plugin apps");
api.apps.register({
id: "notes",
label: { en: "Notes", es: "Notas" },
html: () => PAGE, // HTML autocontenido, como máximo 8 MiB
actions: {
save: {
effect: "write",
inputSchema: { type: "object", properties: { text: { type: "string" } } },
handler: async (input, context) => saveNote(context.sessionId, input),
},
},
busy: () => hasBackgroundJobs(), // evita la expulsión por inactividad del workspace
dispose: () => cancelOwnedJobs(), // síncrono e idempotente
});
},
});- Ids y etiquetas.
idsigue las reglas de las vistas (^[a-z][a-z0-9-]{0,39}$) y es único por complemento; un duplicado lanza un error.labeles un texto o{ en, es }; la web usa el idioma activo. Los nombres de acción cumplen^[A-Za-z][A-Za-z0-9_.-]{0,63}$. - Dónde aparece. La web lista las aplicaciones de los complementos habilitados del proyecto abierto en un grupo Aplicaciones justo encima de Proyectos (el grupo no se muestra si no hay ninguna) y refresca la lista cada 15 segundos. Una lista larga muestra cinco aplicaciones y un botón Mostrar todas (N). Abrir una aplicación sustituye toda el área del chat; el panel de proyectos permanece y Volver al chat devuelve la conversación, su borrador y los adjuntos pendientes. El frame sigue montado al alternar, así que la edición sin guardar se conserva. Otro proyecto es otra instancia. La aplicación usa la sesión actual del proyecto (o la más reciente, o una nueva) y la conserva.
- El frame.
GET /api/sessions/:sid/apps/:plugin/:app/framesirve la página en uniframecon sandbox (allow-scripts allow-downloads allow-modals allow-popups, sinallow-same-origin: no tiene cookies ni acceso al DOM del host). Cada<script>en línea debe llevarnonce="__ALISIO_NONCE__"; el servidor sustituye el marcador por un nonce nuevo en cada petición. La CSP esdefault-src 'none', scripts con nonce, estilos en línea, imágenes, fuentes y mediosdata:/blob:,connect-src blob:,worker-src blob:,base-uri 'none'yform-action 'none': la página no alcanza la red, solo el host. - Mensajes. Al cargar, el host transfiere un
MessagePortal frame con{ type: "alisio.app.connect", locale, visible }(acepta el primer canal que venga dewindow.parent). Por el puerto el frame envía{ type: "request", id, action, input }y recibe{ type: "response", id, result }o{ type: "response", id, error: <código> }; el host también envía{ type: "locale", locale }y{ type: "visibility", visible }. Los códigos sonnot_found,invalid_input,not_permitted,timeout,failed,too_large,too_many_requests,unauthorizedynetwork; nunca llevan entradas ni secretos. Se ejecutan como máximo 8 peticiones a la vez por frame. Los tipos (PluginAppConnectMessage,PluginAppRequestMessage,PluginAppHostMessage,PluginAppErrorCode) están en@alisio/sdk. - Acciones. Cada acción declara un
effect(read,writeoprocess) y uninputSchema(esquema de objeto; se rechazan$ref,$idy$async). Por HTTP una acción esPOST /api/sessions/:sid/apps/:plugin/:app/actions/:actioncon{ input, idempotencyKey? }. El host valida la entrada y aplica la misma puerta de efectos que una llamada a herramienta con las capacidades efectivas de la sesión. Un efecto que la sesión ya concede se ejecuta. Cuando el chat lo preguntaría (una acciónwriteoprocessen una sesión que no es de solo lectura), el host abre la misma aprobación que una llamada a herramienta del chat, con las mismas opciones (permitir una vez, permitir para la sesión, denegar): el aviso dice «Complemento · App quiere escribir archivos (acción)», se muestra sobre la app (el chat está oculto mientras hay una app abierta) y nunca incluye la entrada. «Permitir para la sesión» amplía la política de la sesión igual que el chat, así que las acciones de apps y las herramientas del chat con ese efecto dejan de preguntar. Denegar, no responder, no tener un cliente web abierto (la aprobación se deniega tras la gracia habitual) o un efecto que la sesión prohíbe de plano (sesión de solo lectura) se rechaza conapp_action_denied(403). El timeout de 30 segundos de la acción empieza cuando arranca el handler, no mientras la aprobación está pendiente; la espera de la respuesta tiene el tope del timeout de aprobaciones del chat (10 minutos) y cerrar la petición cancela la aprobación pendiente. El rechazo nombra el efecto para que la app pueda decir qué activar:{ type: "response", id, error: "not_permitted", effect: "write" | "process" }por el puerto yerror.details.effecten el cuerpo del error HTTP. La sesión sale de la ruta, nunca de la entrada (context.sessionId,context.workspace). Se comprueba en cada llamada que el complemento esté habilitado. Límites: 37 MiB de petición, 90 MiB de resultado (app_result_too_large), 30 segundos (app_action_timeout, 504;context.signalaborta). El trabajo largo debe devolver un job enseguida en lugar de retener la petición. Los fallos son unapp_action_failedgenérico (502); las entradas y los resultados nunca se registran. - Modelos y agentes. Los handlers reutilizan
api.models,api.model.completeyapi.sessions; no necesitan un SDK de proveedor ni claves aparte. - Un sandbox de navegador no es un sandbox de Node. El frame está aislado en el navegador, pero el backend del complemento es código de confianza en proceso con los privilegios de Alisio, y el timeout de una promesa no detiene código que consume CPU. Deshabilitar un complemento bloquea de inmediato las acciones nuevas, pero un puerto ya abierto no se revoca al instante.
Consulta examples/plugins/hello-app para una aplicación mínima.
Decision Intelligence, rutas y opciones
api.decisions, api.paths y api.options son opcionales: un núcleo anterior los omite, así que detecta cada uno por presencia y sigue funcionando sin él.
setup(api) {
api.decisions?.registerProvider(myProvider); // Registering never activates it.
const cache = api.paths?.cache; // Per-plugin directory, created on first read.
const device = api.options?.device ?? "cpu"; // From `pluginOverrides[id].options`.
}api.decisionsregistra un proveedor de decisiones y permite que una herramienta de complemento pida decisiones. El usuario activa un proveedor condecisions.provideren la configuración global; instalar el complemento no basta. El contrato, los errores y el ciclo de vida están en Decision Intelligence. Las herramientas reciben el lado consumidor comocontext.decisions?.tryDecide(...), ligado a la ejecución actual. El miembro opcionalapi.decisions?.activate?.(providerId)permite que un complemento pida al host, desde un paso iniciado por el usuario, que un proveedor que registró pase a ser el activo: el usuario confirma una vez y el host guardadecisions.provideren la global (consulta pedir la activación).api.pathsofrecestate(<raíz de estado>/plugins/<id>),config(<config home>/plugins/<id>) ycache(<raíz de estado>/plugins/<id>/cache). La raíz de estado es la que usan el análisis y los artefactos, así que respeta--db. Úsalas en lugar de resolver tú mismo las rutas XDG oALISIO_*.api.optionscontiene el JSON que tú o el usuario pusieron enpluginOverrides[id].optionsde la configuración global (consultapluginOverrides). Es solo global, de como máximo 8 KB, y una instantánea tomada ensetup: cambiarlo requiere reiniciar. No guardes secretos ahí.
Proveedores y consumidores de búsqueda semántica
api.semantic y context.semantic son opcionales: un núcleo anterior los omite, así que detéctalos por presencia y sigue funcionando sin ellos. El núcleo define dos puertos independientes y un servicio que los compone; no incluye ningún motor (ni base de datos vectorial, ni runtime de inferencia, ni modelo). La búsqueda semántica existe solo cuando el usuario instala un complemento que la ofrece.
// Consumer: index and search text without knowing the engine.
async execute(input, context) {
const semantic = context.semantic;
if (!semantic?.available()) return fallbackToFullTextSearch(input); // e.g. SQLite FTS5
await semantic.index("notes", [{ id: "n1", text: "Quarterly budget review" }]);
const matches = await semantic.trySearch("notes", input.query, { topK: 5, minScore: 0.3 });
return matches ?? fallbackToFullTextSearch(input); // null: infrastructure problem
}
// Provider: register the ports (registering never activates them).
setup(api) {
api.semantic?.registerEmbedding({
id: "my-embed",
name: "My embeddings",
modelId: () => "my-org/my-model@q8", // changes whenever the vectors stop being comparable
dimensions: () => 384,
async embed(texts, { kind, signal }) {
// `kind` is "query" or "document": apply the model's own prefixes here.
return runModel(texts, kind, signal); // one Float32Array per text, same order
},
});
api.semantic?.registerStore(myVectorStore); // a `VectorStoreProvider`
}- La activación es del usuario.
semantic.embeddingysemantic.storeen la configuración global eligen los proveedores activos (consultasemantic); una configuración de proyecto no puede. Un proveedor puede ofrecer un paso de configuración iniciado por el usuario que llame aapi.semantic?.activate?.({ embedding, store }): el host pregunta una vez, guarda los ids globalmente, nunca sobrescribe otro proveedor configurado y devuelveneeds_confirmationcuando no hay a quién preguntar (headless). - Las colecciones tienen espacio de nombres por complemento. El host guarda
<pluginId>/<collection>ycollections()lista solo las suyas, sin el prefijo. Evita colisiones accidentales; no es aislamiento: los complementos son código de confianza en proceso. - Los modelos se vigilan. Cada colección recuerda el
modelIdy lasdimensionscon que se creó. Si el usuario cambia de modelo,indexysearchrechazan conmodel_mismatchodimension_mismatch: borra la colección e indexa de nuevo desde tu propio texto fuente, porque solo tú lo conservas.trySearchdevuelvenullen ese caso para que puedas recurrir a otra vía. - Los límites se comprueban antes de llamar a ningún proveedor y fallan con
invalid_request: 1..256 textos porembed(hasta 32768 caracteres cada uno), hasta 1000 documentos porindex,topK1..100, ids de registro^[A-Za-z0-9._:-]{1,128}$, nombres de colección^[a-z0-9][a-z0-9_-]{0,63}$, metadatos de hasta 8 KiB con valores escalares y filtros de igualdad sobre hasta 16 claves de primer nivel. - Las llamadas al proveedor están acotadas. Cada llamada recibe un
AbortSignaly un tiempo límite (semantic.embedTimeoutMs,semantic.queryTimeoutMs); un agotamiento estimeout, el aborto del llamador se propaga como aborto y cualquier fallo del proveedor es unprovider_errorcuyo mensaje no contiene rutas ni texto del usuario. minScoredepende del modelo: las puntuaciones coseno no son comparables entre modelos./semantic(solo lectura, en todas las superficies) muestra los proveedores registrados y activos, el modelo, las dimensiones y elhealth()de cada proveedor (límite de 1 segundo).
Verificadores de Proof
Un complemento puede aportar un verificador para los criterios de aceptación de Proof. El miembro es opcional: detéctalo por presencia y el complemento sigue funcionando en un núcleo sin Proof.
setup(api) {
api.proof?.registerVerifier({
id: "links",
describe: "Checks that the docs have no broken links",
async run(input, ctx) {
const { callId, result } = await ctx.callTool("read_file", { path: "docs/index.md" });
const text = result.content.map((p) => (p.type === "text" ? p.text : "")).join("");
return { ok: !text.includes("](broken)"), outputHash: sha256(text), callId };
},
});
}Un usuario enlaza un criterio con { "kind": "plugin", "verifierId": "<pluginId>:links" }. El resultado debe llevar evidencia referenciable: outputHash (sha256 en hexadecimal de lo que respalda el veredicto) y, si hizo llamadas a herramientas del núcleo, el callId de la que lo respalda. Sin un outputHash válido, o con un callId que la sesión nunca hizo, el criterio pasa a manual_review. Si run lanza un error o tarda más de 60 s, el criterio queda como estaba. Un verificador no gana permisos: ctx.callTool ejecuta una herramienta del núcleo por la misma compuerta que el agente (una llamada rechazada vuelve con isError). Es código de confianza dentro del proceso, no un sandbox, y nunca crea ni edita criterios.
Sesiones hijas
api.sessions es un servicio genérico para trabajo delegado: una sesión hija es una conversación separada y persistida (contexto nuevo) con un vínculo a su padre, ejecutada por el mismo runner con permisos reducidos. Un hijo nunca puede obtener una capacidad, herramienta o aprobación que su padre no tenga, y abortar la ejecución de un padre aborta sus descendientes en ejecución. El núcleo no contiene lógica de agentes; el complemento de subagentes está construido sobre este servicio.
| Miembro | Descripción |
|---|---|
spawn(spec) | Crea una sesión hija (ChildSessionSpec) y devuelve ChildSessionInfo |
create(spec) | Resuelve spec.model, si existe, y crea un hijo vinculado al proveedor; úsalo para reemplazos de modelo |
run(id, prompt, { signal? }) | Ejecuta un turno en el hijo; resuelve un ChildRunResult (status, text, usage, error?) |
get(id), children(parentId) | Información de la sesión (id, parentId, depth, agent, title, status, model, workspace, usage, capabilities, marcas de tiempo) |
ancestors(id) | IDs de los ancestros, del más cercano al más lejano |
cancel(id) | Cancela una ejecución y todos sus descendientes en ejecución; devuelve cuántos estaban en ejecución |
enqueue(id, text) | Encola un mensaje de usuario para el siguiente turno de la sesión (o la siguiente ejecución si está inactiva) |
isRunning(id) | Si la sesión está en ejecución |
capabilities(id) | write, process, approvals y readOnly efectivos |
model(id), workspace(id) | Modelo que heredaría un hijo nuevo; workspace de la sesión |
setStatus(id, status) | Establece un SessionStatus: queued, running, completed, failed, cancelled o interrupted |
Campos de ChildSessionSpec: parentId, id?, title, agent (etiqueta que se muestra en las interfaces y las aprobaciones), instructions?, tools?: { allow?, deny? } (* permite todas las herramientas del padre), model?, readOnly?, permission?: { write?, process? } (allow, ask o deny), workspace? (por ejemplo, un worktree de git), maxTurns?, timeoutMs? y maxTokens?.
sessions.run resuelve con { id, status, text, usage, error? }. Un hijo que agota su tope de turnos NO es un fallo: resuelve con status: "completed" y turnsExceeded: true, y text contiene un informe parcial utilizable (solo los errores reales, las cancelaciones y los tiempos de espera producen un estado de fallo).
api.tools.register({
name: "second_opinion",
description: "Ask a read-only child session to review a file.",
effect: "external",
concurrent: true,
inputSchema: {
type: "object",
properties: { path: { type: "string" } },
required: ["path"],
additionalProperties: false,
},
async execute(input, ctx) {
if (!ctx.session) return textResult("needs a session", true);
const child = api.sessions.spawn({
parentId: ctx.session,
title: `Review ${String(input.path)}`,
agent: "reviewer",
instructions: "Review the file and report bugs only.",
tools: { allow: ["read_file", "search_text"] },
readOnly: true,
maxTurns: 10,
});
const result = await api.sessions.run(child.id, `Review ${String(input.path)}`, {
signal: ctx.signal,
});
return textResult(result.text || result.error || result.status, result.status !== "completed");
},
});PanelProvider tiene un title, nodes({ sessionId }), que devuelve PanelNodes (id, parentId?, label, color?, status, startedAt?, endedAt?, tokens?, detail?, sessionId?), y un action("cancel" | "background", nodeId, { sessionId }) opcional.
Timeouts de hooks y aislamiento de fallos
- Los hooks de compactación y de inicio de sesión se ejecutan con
pluginHooks.timeoutMs(por defecto 15 000 ms); los de fin de sesión, conpluginHooks.sessionEndTimeoutMs(por defecto 10 000 ms). - Cada hook recibe un
AbortSignalque se aborta cuando vence el tiempo límite. - Un fallo o timeout se registra como evento
plugin_hook_failedy el núcleo continúa sin la contribución de ese complemento. - Los hooks se ejecutan en el mismo proceso: el timeout detiene la espera y señala el
AbortSignal, pero no puede detener código síncrono bloqueante.
Cierre
dispose()se ejecuta cuando Alisio se cierra: salir de la interfaz de terminal,SIGINT,SIGTERMySIGHUP(interfaz de terminal,alisio serveyalisio run) y el final de una ejecución. No se ejecuta al deshabilitar un complemento ni con/reload, porque ambos requieren reiniciar.- Los complementos se liberan en paralelo y cada uno está limitado por
pluginHooks.disposeTimeoutMs(2000 ms por defecto, de 100 a 10000); undispose()lento o colgado no retrasa a los demás, y su fallo se informa sin detenerlos. Termina con holgura dentro de ese límite. - Una muerte súbita (
SIGKILL, una caída) no puede cubrirse: un complemento que posee un proceso del sistema operativo debe limpiar por sí mismo los que queden huérfanos.
Puntos de extensión
Los puntos de extensión permiten que un complemento reemplace una parte del host con su propio proveedor. Están tipados en ExtensionPoints; los puntos nuevos se añaden ahí sin romper los existentes.
| Punto | Tipo de proveedor | Por defecto |
|---|---|---|
mascot | MascotProvider: { id, render(ctx: MascotContext) } que devuelve un string o string[] | DefaultAlisioMascot (alisio.default) |
startup-screen | StartupScreenProvider: { id, render(ctx: StartupContext): string[] } | DefaultStartupScreen (alisio.default) |
websearch | SearchProvider: { id, search(query, options?: { signal? }): Promise<SearchResult[]> }, SearchResult = { title, url, snippet } | La cadena integrada: un websearch.provider configurado, o si no una instancia pública de SearXNG (consulta Herramientas y permisos) |
Registra un proveedor de forma imperativa, o decláralo en el objeto del complemento:
import { definePlugin } from "@alisio/sdk";
import { kiteMascot } from "./kite.js";
// Imperative, with a priority
export default definePlugin({
id: "kite-mascot",
version: "0.1.0",
apiVersion: 1,
setup(api) {
const dispose = api.extensions.register("mascot", kiteMascot, { priority: 10 });
// dispose() unregisters it; everything is also removed when the plugin unloads.
},
});
// Declarative, at priority 0
export const declarative = definePlugin({
id: "kite-mascot-declarative",
version: "0.1.0",
apiVersion: 1,
extensions: { mascot: kiteMascot },
setup() {},
});api.extensions.register(point, provider, { priority })devuelve una función que anula el registro.priorityes un número finito,0por defecto; un proveedor necesita unid(lo único que comparten todos los puntos); un punto renderizable comomascotademás necesita unrenderque funcione, comprobado cuando el host lo llama de verdad, igual que hoy unrenderque lanza excepción ya recurre al valor por defecto.- El campo declarativo
Plugin.extensionses azúcar sintáctico paraapi.extensions.register(point, provider)con prioridad0, aplicado antes de ejecutarsetup.
Resolución
Para cada punto, el host elige exactamente un proveedor, de forma determinista:
- la mayor
priority; - después, el
iddel complemento en orden lexicográfico (los complementos se identifican porid, no por el nombre del paquete); - después, el orden de registro dentro del mismo complemento.
El orden de carga entre complementos nunca influye. Los valores por defecto integrados solo se usan cuando no hay nada registrado, o como reemplazo de un proveedor que falla. Cuando varios proveedores empatan en la prioridad ganadora, el host informa un diagnóstico extension_conflict con point, winner y losers (como IDs plugin/provider). Los conflictos y los proveedores resueltos se muestran en /stats y en alisio plugins doctor:
alisio plugins doctor --plugin ./dist/index.jsContextos tipados
| Tipo | Campos |
|---|---|
TerminalCapabilities | color (ANSI SGR permitido), unicode (no ASCII permitido), columns, interactive |
MascotContext | terminal, version |
PluginMetadata | id, version, builtin |
StartupFact | label, value |
StartupContext | version, cwd (ya abreviado), model?, provider? (solo el host, nunca credenciales), userName?, terminal, plugins (PluginMetadata[]), mascot (la mascota resuelta y validada), tips, facts? (datos del host como el acceso y el estado de la memoria) |
StartupContext.mascot siempre puede llamarse con seguridad: una pantalla personalizada puede reutilizar la mascota ganadora.
Reglas de seguridad
- Los proveedores reciben solo su contexto: no deben leer globales, variables de entorno ni el sistema de archivos. Respeta
terminal.color,terminal.unicodeyterminal.columns. - Un proveedor que lanza una excepción, devuelve algo distinto de un string o un array de strings, devuelve demasiadas líneas (12 para una mascota, 60 para una pantalla) o tarda más de 250 ms se reemplaza por el valor por defecto, y se registra un diagnóstico
plugin_hook_failed. El renderizado es síncrono, así que un proveedor lento no puede interrumpirse: su salida se descarta después. - La salida se sanea: solo se conservan las secuencias de color SGR, y solo cuando
colores true; se eliminan las demás secuencias de escape y los caracteres de control; los tabuladores se convierten en espacios; los caracteres no ASCII se convierten en?cuandounicodees false. Cada línea se recorta acolumns. - La pantalla por defecto se construye con funciones de sección reutilizables que exporta
@alisio/core:titleSection,welcomeSection,infoSection,pluginsSection,tipsSection,mascotSection,composeSideBySide,shortenPathystartupTips(determinista para una semilla dada). Muestra la mascota junto a la información cuando la terminal es lo bastante ancha, y apiladas en caso contrario.
Ejemplo: mascota cometa
El repositorio incluye un paquete de ejemplo en examples/plugins/custom-mascot (alisio-plugin-kite-mascot, id de complemento kite-mascot). Solo depende de @alisio/sdk (dependencia peer) y distribuye JavaScript.
import { definePlugin, type MascotProvider, type StartupScreenProvider } from "@alisio/sdk";
/** A kite riding the trade winds. Honors unicode/color/columns from the context only. */
export const kiteMascot: MascotProvider = {
id: "kite",
render({ terminal }) {
if (terminal.columns < 40) return terminal.unicode ? "◇~ kite" : "<>~ kite";
const art = terminal.unicode
? [" ◢◣", " ◢██◣", " ◥██◤", " ◥◤", " ╲", " ∿∿"]
: [" /\\", " / \\", " \\ /", " \\/", " \\", " ~~"];
return terminal.color ? art.map((line) => `\u001b[35m${line}\u001b[0m`) : art;
},
};
/** Optional compact screen that reuses whichever mascot won resolution. */
export const compactScreen: StartupScreenProvider = {
id: "kite.compact",
render(ctx) {
const mascot = [ctx.mascot.render({ terminal: ctx.terminal, version: ctx.version })].flat();
return [
...mascot,
`Alisio ${ctx.version} · ${ctx.model ?? "no model"} · ${ctx.plugins.length} plugin(s)`,
...ctx.tips.slice(0, 1),
];
},
};
export default definePlugin({
id: "kite-mascot",
version: "0.1.0",
apiVersion: 1,
setup(api) {
// Priority 10 beats plugins registering at the default priority 0.
api.extensions.register("mascot", kiteMascot, { priority: 10 });
// Uncomment to also replace the whole startup screen:
// api.extensions.register("startup-screen", compactScreen);
},
});npm run build
alisio --plugin ./examples/plugins/custom-mascot/dist/index.jsInicio de sesión del proveedor (auth)
Una registración de proveedor puede declarar auth: ProviderAuthMethod[] para que el usuario inicie sesión con una cuenta en lugar de pegar una clave. El host ejecuta el flujo, guarda la sesión, la renueva y la redacta; el complemento solo habla con su propio servicio.
api.providers.register({
id: "my-service",
name: "My service",
fields: [], // sin campo secreto obligatorio: el proveedor es solo de inicio de sesión
auth: [
{
id: "browser", // ^[a-z0-9_-]{1,40}$, único dentro de la registración
label: "Cuenta de My service",
kind: "oauth",
description: "Abre la página de inicio de sesión de My service.",
async start(ctx) {
const loopback = await ctx.loopback?.({ port: 0, path: "/callback", state }).catch(() => undefined);
ctx.show({ type: "auth_url", url: authorizationUrl, instructions: "Aprueba el acceso." });
const code = loopback ? (await loopback.wait()).code : await ctx.prompt({ message: "Pega la dirección a la que te llevó", secret: true });
const tokens = await exchange(code, ctx.signal);
return { type: "oauth", access: tokens.access, refresh: tokens.refresh, expires: tokens.expiresAt };
},
async refresh(credential, signal) {
/* devuelve la credencial renovada; lanza un error con retryable = true si el fallo es temporal */
},
},
],
create: (request) => makeClient(() => request.oauth?.get()), // request.oauth existe solo si se declara `auth`
});start(ctx)resuelve con la credencial o rechaza;ctx.signalse aborta al cancelar, reemplazar o expirar (15 minutos).ctx.showacepta pasosauth_url,device_codeyprogress;ctx.promptpide un valor al usuario y resuelveundefineddonde nadie puede responder (alisio run).ctx.loopbackexiste solo cuando el host puede suponer que el navegador está en la misma máquina (Alisio Desktop, o un servidor enlazado a loopback); si no, usaprompt. Un túnel puede romper esa suposición, así que ofrece también pegar la dirección.- Las URL de los pasos deben ser
https:(ohttp:enlocalhost/127.0.0.1) sin credenciales; si no, el flujo termina conunsafe_url. - Marca
unofficial: truecuando el proveedor no haya autorizado oficialmente el método: la interfaz web muestra un aviso de riesgo fijo y pide confirmación. No lo marques en un método oficial. label,description,instructionsyprompt.messagese muestran como texto plano. Pon en ellos cualquier texto propio del proveedor: el host solo añade botones y estados genéricos.- Sin
refreshla sesión no se renueva: al caducar, el perfil necesita iniciar sesión de nuevo. Un rechazo conretryable === truees un fallo temporal; cualquier otro es permanente. Lametadatade la renovación se fusiona sobre la guardada. request.oauth.get()devuelve una credencial válida (renovando antes) einvalidate("sign_in_required")avisa al host de que el proveedor rechazó la sesión.- Un complemento es código de confianza que corre en el proceso del host: puede leer la sesión guardada. El host reduce la superficie (el asa existe solo con
authy está ligada al perfil), pero un complemento nunca es un sandbox.
Ejemplo: proveedor de búsqueda personalizado
examples/plugins/custom-websearch (alisio-plugin-brave-websearch, id de complemento brave-websearch) envuelve Brave Search con una clave de API proporcionada por el usuario mediante el punto de extensión websearch — una referencia copiable para "trae tu propio motor de búsqueda":
import { definePlugin, type SearchProvider, type SearchResult } from "@alisio/sdk";
export function braveSearchProvider(apiKey: string): SearchProvider {
return {
id: "brave-example",
async search(query, options): Promise<SearchResult[]> {
const url = new URL("https://api.search.brave.com/res/v1/web/search");
url.searchParams.set("q", query);
const res = await fetch(url, {
headers: { "X-Subscription-Token": apiKey, Accept: "application/json" },
signal: options?.signal,
});
if (!res.ok) throw new Error(`Brave Search request failed: HTTP ${res.status}`);
const body = await res.json();
return (body.web?.results ?? []).map((r: { title?: string; url?: string; description?: string }) => ({
title: r.title ?? "",
url: r.url ?? "",
snippet: r.description ?? "",
}));
},
};
}
export default definePlugin({
id: "brave-websearch",
version: "0.1.0",
apiVersion: 1,
setup(api) {
const apiKey = process.env.BRAVE_SEARCH_API_KEY;
if (!apiKey) throw new Error("brave-websearch: set BRAVE_SEARCH_API_KEY");
api.extensions.register("websearch", braveSearchProvider(apiKey), { priority: 10 });
},
});npm run build
export BRAVE_SEARCH_API_KEY=...
alisio --plugin ./examples/plugins/custom-websearch/dist/index.js --allow-externalCargar complementos
Los complementos solo se cargan desde orígenes de confianza explícita. Alisio instala un complemento solo cuando se lo pides: el comando alisio install, los botones de instalación de la interfaz web y de la aplicación de escritorio, o la herramienta plugin_install tras su aprobación.
| Origen | Confianza |
|---|---|
--plugin <path>, --plugin <package> | Confianza explícita en esa entrada y sus dependencias |
plugins en el archivo de configuración | El propio archivo es de confianza (configuración global, --config o --trust-project) |
<config home>/plugins/ (archivos o directorios) | Complementos globales: código personal de confianza |
<workspace>/.alisio/plugins/ | Solo con --trust-project |
alisio --plugin ./my-plugin.js
alisio --plugin alisio-plugin-foo{ "plugins": ["alisio-plugin-foo"] }--read-only desactiva los complementos ejecutables (externos). Los complementos integrados no se ven afectados.
Resolución. Una entrada con forma de ruta (empieza por ., es absoluta, contiene \, termina en una extensión JS/TS, o contiene / sin empezar por @) es un archivo o directorio. Cualquier otra cosa es un nombre de paquete npm, que se busca en node_modules desde el directorio del proyecto hacia arriba y después en las raíces globales: las entradas de NODE_PATH y el prefijo global del Node/npm en ejecución.
La palabra clave alisio-plugin es obligatoria. Un paquete sin "keywords": ["alisio-plugin"] en su package.json se rechaza, para que un error tipográfico no cargue un paquete no relacionado. La entrada se toma de exports["."] (condiciones import, node o default), después de main y, por último, de ./index.js.
Icono del complemento (opcional). La interfaz web y la app de escritorio muestran un icono junto a cada complemento. Uno del catálogo usa el icon del catálogo. Cualquier otro complemento de npm puede incluir el suyo declarando en package.json una ruta relativa al paquete, y listando el archivo en files: "alisio": { "icon": "assets/icon.svg" }. El archivo debe ser SVG, PNG, WebP o JPEG de 256 KB como máximo y quedarse dentro del paquete (se ignora una ruta que salga de él, una ruta absoluta o un enlace simbólico hacia fuera); el servidor lo entrega solo como imagen, nunca como página. Sin icono (o si no carga) se muestra el icono predeterminado. Los complementos integrados, de proyecto y de archivo usan el predeterminado salvo que sean paquetes de npm que declaren uno.
Un directorio indicado como ruta debe contener un manifiesto alisio-plugin.json que declare su entrada (un paquete también puede incluirlo); la entrada debe permanecer dentro del directorio:
{ "apiVersion": 1, "entry": "./index.js" }Comprueba lo que registra un complemento con:
alisio plugins list
alisio plugins doctor --plugin ./my-plugin.jsInstalar complementos desde npm
La CLI puede instalar un paquete npm en el directorio GLOBAL de complementos de Alisio (<config home>/plugins, por ejemplo ~/.config/alisio/plugins) y registrar su NOMBRE npm en el array plugins de la configuración global — la misma entrada que hoy puedes escribir a mano:
alisio install npm:plugin-openrouter # última versión
alisio install npm:@scope/plugin-x@1.2.3 # una versión fijada
alisio install plugin-openrouter # el nombre pelado es igual que npm:
alisio install npm:plugin-openrouter --update # actualiza un complemento instalado a @latest
alisio install --update # actualiza TODOS los complementos instalados en una sola llamada de npmLa especificación se valida antes de cualquier operación de red: npm:<paquete>[@<versión>], o un nombre de paquete pelado; solo se permiten letras, dígitos, ., _, -, más / para nombres scoped y @ para una versión. Los prefijos desconocidos (git:, file:, URLs registry:, ...) se rechazan con un error claro. Un fallo de instalación nunca vuelve a ejecutar npm en silencio y la salida de npm fallida se sanea (sin tokens/secretos); el error nombra el paquete y el comando de reintento exacto.
Dónde aterriza. El paquete se instala con npm install --prefix <config home>/plugins en <config home>/plugins/node_modules/<paquete> y su NOMBRE (nunca la ruta resuelta en el filesystem) se añade al array plugins de <config home>/config.json (escritura atómica, campos no relacionados conservados, sin duplicados). alisio plugins list muestra los paquetes instalados junto a los complementos de archivos/directorios.
| Aspecto | Comportamiento |
|---|---|
| Alcance global | Una instalación sirve a todos los proyectos desde los que ejecutes Alisio |
| Carga | El complemento se carga como código ejecutable en proceso dondequiera que carguen los complementos globales; debe declarar la keyword alisio-plugin para poder cargarse |
| Confianza del proyecto | La configuración y los complementos PROPIOS de un proyecto solo cargan en proyectos de confianza (--trust-project o el aviso de confianza de una sola vez) |
--read-only | Se rechaza instalar y el complemento nunca carga (los complementos ejecutables están desactivados) |
| Reinstalación | Si el paquete ya está instalado, el comando lo indica y sugiere --update en lugar de volver a ejecutar npm |
| Scripts | npm install puede ejecutar scripts de ciclo de vida del paquete con tus privilegios — Alisio avisa y exige confirmación en terminal interactiva |
Headless / --json | Nunca pregunta: sin un --yes explícito (o --trust-plugin) falla con un error accionable antes de ejecutar npm |
| Errores | Salida npm saneada, nombre del paquete y comando de reintento exacto en el mensaje final |
--update sin spec | Actualiza a @latest todos los complementos instalados en el directorio global de complementos con una sola llamada a npm install --prefix; mismas reglas de confirmación, --yes y --read-only; sin complementos instalados lo indica y termina con código 0 |
La instalación es una acción global del usuario: no concede nada a ningún proyecto. La carga sigue la política existente de complementos ejecutables — el aviso de confianza de una sola vez (o --trust-project) es lo que permite a un proyecto cargar su propia configuración y complementos, y --read-only desactiva los complementos ejecutables por completo.
Conflictos de dependencias peer (ERESOLVE)
Los complementos antiguos ya instalados pueden fijar un @alisio/sdk anterior mediante su rango peer, por lo que npm rechaza instalar un complemento nuevo que necesita un SDK más reciente (ERESOLVE ... Conflicting peer dependency). Alisio explica entonces la causa, lista los complementos instalados implicados y no muestra la línea genérica de reintento, porque el reintento fallaría igual. Actualiza todos los complementos instalados juntos (hacerlo uno a uno vuelve a chocar con el mismo conflicto) y luego repite la instalación original:
alisio install --update
alisio install npm:@alisio/plugin-layaAlisio nunca sugiere --force ni --legacy-peer-deps para esto.
En la interfaz web (Ajustes → Complementos → Complementos instalados) esto es automático. Actualizar todo ejecuta un único npm install combinado de todos los complementos del catálogo con actualización. Actualizar en un solo complemento prueba primero ese complemento solo; si npm informa ERESOLVE, Alisio reintenta exactamente una vez como una sola instalación de ese complemento más los demás complementos del catálogo instalados que tienen una versión más reciente y compatible (los complementos locales .tgz y los que ya están al día no se tocan) y lista los complementos que se actualizaron junto con él. Si el conflicto persiste, no se intenta nada más y el error nombra los complementos implicados.
El agente puede instalar un complemento por ti
En la TUI, el modelo puede instalar un complemento a petición mediante la herramienta del host plugin_install: valida la especificación, ejecuta la misma rutina de instalación que alisio install y responde con el nombre del paquete, la versión instalada, la entrada de configuración y la ruta, más la nota de confianza. La herramienta usa el efecto process, así que pasa por la compuerta de permisos habitual — el aviso de aprobación en la TUI, o --allow-process en headless — y nunca está disponible con --read-only. Consulta Herramientas y permisos.
Tú: instala el complemento plugin-openrouter
Agente: (llama a plugin_install con spec "npm:plugin-openrouter", tú lo apruebas)
Agente: Instalado el complemento "plugin-openrouter" v1.2.3 en
~/.config/alisio/plugins/node_modules/plugin-openrouter y añadido al array
"plugins" de ~/.config/alisio/config.json. El complemento carga como código
personal de confianza; --read-only impide que cargue. Ejecuta `alisio plugins
list` para inspeccionarlo.Instalar desde la web y desde un archivo
La interfaz web y la aplicación de escritorio instalan, actualizan y eliminan complementos en Ajustes → Complementos, con la misma advertencia de seguridad que la CLI. Las pantallas están en Catálogo de complementos, instalación y actualizaciones.
- Desde el catálogo. Un complemento del catálogo se instala con un clic, en la versión del catálogo (la última del registro cuando npm aún no sirve esa versión). Solo se pueden instalar así los paquetes listados en el catálogo, y
--read-onlylo rechaza. - Actualizar en el sitio. Instalar un paquete que ya está instalado lo reemplaza (actualización, reinstalación o versión anterior) y conserva una sola entrada en el array
plugins. Una actualización fallida restaura la versión anterior. - Desde un archivo (complementos privados). Un complemento que nunca se publica se distribuye como tarball. Se genera desde la carpeta del complemento (o deja que el creador de complementos lo cree y lo empaquete por ti):
pnpm pack
npm packDespués elige el .tgz en Instalar desde archivo. Alisio lee solo package/package.json (un lector gzip y tar propio: no hace falta el binario tar) y comprueba que sea un complemento de Alisio: la keyword alisio-plugin, un nombre npm válido y una versión semver, y un punto de entrada (exports o main). El archivo puede pesar hasta 50 MB y package.json hasta 1 MB; una entrada cuya ruta pudiera salir de la carpeta del paquete rechaza el archivo. Un rango del peer @alisio/sdk que no coincide con el SDK instalado es una advertencia, y los scripts preinstall, install o postinstall declarados se destacan.
- Dónde vive un complemento de archivo. El tarball se copia a
<config home>/plugins/local-packages/<nombre>-<versión>.tgzy npm instala desde esa copia, así que la dependenciafile:local-packages/...de<config home>/plugins/package.jsonnunca apunta a un archivo que pueda moverse. Actualizar reemplaza la copia guardada; desinstalar la elimina; un complemento instalado desde una ruta fuera delocal-packagesse migra en su siguiente actualización. La carga se guarda temporalmente en<state home>/tmp/plugin-uploads/y se borra tras la instalación, un fallo o una cancelación; lo que quede con más de una hora se limpia. Una instalación fallida se revierte: la copia guardada, un paquete parcial y la entrada de configuración se restauran. - Reinicio tras una actualización. Node conserva el código de un complemento que ya cargó, así que la actualización de un complemento cargado solo se ejecuta tras un reinicio. El resultado lo indica; la aplicación de escritorio puede reiniciarse sola y
alisio servedebes reiniciarlo tú. - npm. La CLI usa el
npmde tuPATH. La aplicación de escritorio incluye npm (ALISIO_NPM_CLIlo señala), así que instala complementos sin Node.js en el sistema. Sin ninguno de los dos, el error dice: npm was not found: install Node.js, or use the desktop app, which bundles it. - Desinstalar.
DELETE /api/plugins/package/:name(el botón Desinstalar) elimina un complemento de npm del directorio global de complementos y su nombre de la configuración; los complementos integrados y los de proyecto o locales no se pueden desinstalar desde la web.
Creador de complementos
El creador de complementos de la interfaz web construye complementos a partir de una petición en lenguaje natural. Es una herramienta para usuarios no técnicos; esta sección es la referencia de lo que genera y expone.
Proyectos y el agente creador
Los proyectos viven en <config home>/plugin-creator/projects/<id>/ (<id> son 12 caracteres hexadecimales); el registro de metadatos, el último paquete y la skill incluida se guardan junto a ellos en records/, builds/ y skills/. Cada proyecto es además el workspace de su sesión creadora, que ejecuta el agente plugin-creator con el preset de permisos ask y la skill incluida alisio-plugin-creator. El agente tiene dos herramientas acotadas en lugar de un shell: plugin_project (internal: validación estática, estadísticas y pasos de progreso) y plugin_project_run (process, así que pasa por la aprobación normal: la suite node --test del proyecto o la preparación completa, con entorno depurado y límite de tiempo). Las aprobaciones de una sesión creadora solo se muestran en su página mientras está abierta.
Estructura del complemento generado
package.json name, version, type: module, alisio.pluginId / icon / cover
src/index.js ESM simple + JSDoc, el export por defecto es el objeto Plugin
test/harness.js un PluginAPI falso con una base node:sqlite real en memoria
test/*.test.js suites de node:test (npm test = node --test test/)
assets/icon.svg icono provisional de 64x64
assets/cover.svg portada provisional de 1280x720 con el nombre del complemento
README.mdEl contrato: JavaScript ESM simple, sin paso de compilación, sin dependencias en tiempo de ejecución, @alisio/sdk solo como peer opcional (nada lo importa en ejecución), la palabra clave alisio-plugin, un id de complemento con prefijo local. y la versión solo en package.json (src/index.js la lee). Los paquetes se llaman alisio-plugin-local-<slug>-<id> y son privados (UNLICENSED) hasta que los publiques.
Icono y portada
package.json apunta alisio.icon y alisio.cover a archivos dentro del paquete (y dentro de files). La plantilla escribe un icono SVG y una portada con el nombre visible escapado en XML, acortado a partir de 40 caracteres. El modelo puede reemplazarlos por un SVG optimizado (o un PNG, WebP o JPEG); la validación detecta el contenido, limita el tamaño (icono 256 KB, portada 512 KB) y rechaza SVG inseguro: scripts, atributos de manejadores de eventos, foreignObject, imágenes incrustadas, destinos externos en href o url(...), @import y entidades XML.
Validación y compatibilidad
Una preparación valida de forma estática, ejecuta las pruebas y una sonda que importa el complemento, y luego empaqueta un .tgz determinista. Los hallazgos tienen códigos estables (por ejemplo sdk_incompatible, api_member_unknown, forbidden_import, template_example, svg_unsafe, tests_missing). El rango de peerDependencies["@alisio/sdk"] debe aceptar el SDK instalado (los proyectos nuevos usan el rango con acento circunflejo de su minor, como ^0.11.0) y los miembros de PluginAPI que usan las fuentes se comprueban contra esa versión. Límites: 500 archivos, 5 MB por proyecto, 1 MB por archivo, 8000 caracteres por petición, 120 s por ejecución de pruebas. Preparar ejecuta el código del proyecto con los privilegios del usuario; no es un sandbox, y node --test tampoco lo es.
Rutas HTTP
Todas las rutas cuelgan de /api/plugin-creator/projects y toda mutación se rechaza con --read-only.
| Método y ruta | Propósito |
|---|---|
GET / y POST / | Lista los proyectos; crea uno desde request, con displayName y model opcionales |
GET /:id y GET /:id/status | Un proyecto con su estado; estado de instalación y si hay una actualización o un paquete listo |
DELETE /:id | Elimina (uninstall: true también desinstala el paquete) |
POST /:id/session | Abre o reanuda la sesión creadora, opcionalmente con una request y un model nuevos |
GET /:id/files y GET /:id/files/* | Lista de archivos y un archivo de texto (solo lectura, acotado) |
GET /:id/icon | El icono del proyecto |
POST /:id/build | Valida, prueba y empaqueta; un fallo es 422 con la respuesta de la preparación |
GET /:id/package | Descarga el .tgz (plugin_not_built si está desactualizado) |
POST /:id/install | Vuelve a preparar y luego instala con el trabajo de instalación de archivo local; requiere confirmed: true |
Complementos TypeScript
- Los complementos publicados en npm deben distribuir JavaScript.
- Los complementos
.tslocales cargados con--pluginfuncionan en Bun (y en el binario independiente) y en Node.js >= 22.18 mediante type stripping, que solo admite sintaxis borrable (sinenum,namespaceni parameter properties). - En versiones anteriores de Node.js, Alisio falla con un error claro que pide Bun, Node.js >= 22.18 o una compilación a JavaScript.
Paquete de complemento de ejemplo
alisio-plugin-hello/
├── package.json
├── tsconfig.json
└── src/
└── index.tspackage.json:
{
"name": "alisio-plugin-hello",
"version": "0.1.0",
"description": "Example Alisio plugin",
"keywords": ["alisio-plugin"],
"license": "MIT",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": ["dist"],
"scripts": {
"build": "tsc -p tsconfig.json",
"prepublishOnly": "npm run build"
},
"peerDependencies": {
"@alisio/sdk": "^0.2.0"
},
"devDependencies": {
"@alisio/sdk": "^0.2.0",
"typescript": "^5.9.0"
}
}Usa la última versión publicada de @alisio/sdk al crear tu propio complemento — consulta npm view @alisio/sdk version para saber cuál es. Los complementos deben declarar @alisio/sdk como dependencia peer con ^0.2.0: mientras Alisio sea 0.x, un rango con circunflejo solo acepta su propia versión menor, porque una versión menor nueva puede incluir cambios incompatibles, así que adoptas cada versión menor de forma explícita. Un complemento que deba funcionar con ambas líneas puede declarar "^0.1.0 || ^0.2.0"; los complementos publicados para 0.1 (como @alisio/plugin-deepseek) declaran ^0.1.0 hasta que se actualicen.
tsconfig.json:
{
"compilerOptions": {
"target": "ES2023",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"declaration": true,
"outDir": "dist",
"rootDir": "src",
"skipLibCheck": true
},
"include": ["src"]
}src/index.ts:
import { definePlugin, textResult } from "@alisio/sdk";
export default definePlugin({
id: "acme.hello",
version: "0.1.0",
apiVersion: 1,
setup(api) {
let greetings = 0;
api.tools.register({
name: "hello",
description: "Greets a person by name.",
effect: "read",
inputSchema: {
type: "object",
properties: { name: { type: "string" } },
required: ["name"],
additionalProperties: false,
},
async execute(input) {
greetings += 1;
api.ui.status("greetings", `hello ${greetings}`);
return textResult(`Hello, ${String(input.name)}!`);
},
});
api.commands.register("greetings", async () => `Greetings so far: ${greetings}`, {
description: "Show how many greetings were sent",
});
},
});Compílalo y pruébalo:
npm install
npm run build
alisio --plugin ./dist/index.js # local build, as an explicit path
npm publish # then: npm i -g alisio-plugin-hello
alisio --plugin alisio-plugin-hello # or add it to "plugins" in the configurationEn la TUI el comando está disponible como /acme.hello:greetings, y el modelo ve la herramienta con su nombre con prefijo.
