Developing Alisio plugins
Español · English
This guide covers first-party packages in this monorepo and independent plugins published from another repository. Choose the path first; the plugin contract and safety limits apply to both. The canonical upstream reference for the SDK and PluginAPI is the Alisio plugin documentation. This catalog guide focuses on the packaging, publication and listing workflow rather than duplicating the complete API reference.
Everything in this guide was checked against the upstream source (docs/plugins.md, packages/sdk/src/index.ts, packages/core/src/plugins/host.ts, packages/core/src/runtime/modules.ts, packages/core/src/plugins/install.ts). Where the upstream documentation and the source disagree, this guide follows the source and calls it out as a documentation caveat.
- Choose your path
- Third-party quick path
- Catalog inclusion
- Catalog JSON
- First-party quick path
- The contract
- Capability surface
- Hard limits: how far plugins reach
- The most likely mistakes
- Package shape and publishing
- Installing and smoke-testing
- SDK version pinning
- Testing in this repository
- Documentation caveats
Choose your path
| You are building… | Work here? | Release and catalog behavior |
|---|---|---|
An official, first-party @alisio/plugin-* package | Yes — create packages/<name>/. | The workspace checks, Changesets, release tooling, and automatic local catalog discovery apply. |
| An independent plugin owned by your team | No — use your own repository and publish it to npm. | You choose your tooling and release process. It is installable after npm publication; catalog listing requires maintainer review and a registry pull request. |
Universal ecosystem recommendations are marked below. Rules described as first-party only are enforced by this repository's workspace tooling and must not be copied as claims about every external plugin.
Third-party quick path
Use this path when your package belongs in another repository. It does not require a fork of this monorepo.
- Create an ESM Node package and implement the default
definePlugin(...)export described in the contract. - Build JavaScript and declarations into
dist, test the built package, and inspect the tarball. - Publish the package to npm, then install its published version with Alisio and run
plugins doctor. - If you want a listing here, follow Catalog inclusion. npm publication alone does not add a package to this catalog.
External package baseline
These are the compatible, recommended defaults for an independently published plugin. The host requires the alisio-plugin keyword to recognize an npm package; the other entries are ecosystem or quality recommendations, except where your own package policy makes them mandatory.
| Concern | Recommended external-package baseline |
|---|---|
| Name | Prefer @your-scope/alisio-plugin-<name>. @alisio/plugin-* is reserved for first-party packages; the catalog registry can list any valid npm package name. |
| Metadata | Provide description, license, repository, homepage, and bugs so users and catalog maintainers can assess the package. |
| Host SDK | Put @alisio/sdk in both peerDependencies and devDependencies; do not import @alisio/core or another plugin. |
| Runtime and modules | Use ESM ("type": "module"), target Node >=22.16, and expose ./dist/index.js plus ./dist/index.d.ts. |
| Published contents | Include dist, a concise README.md with install/use instructions, an MIT LICENSE (or clearly declared alternative), and every resource or cover the plugin registers. |
| Quality | Keep unit tests; cover a happy path and an error/boundary path for each functional scenario. |
| Trust and data | Plugins run in-process, not in a sandbox. Validate untrusted names, paths, and child-session output; persist durable data atomically; avoid credentials and machine-specific paths in source and the tarball. Optional built-in integrations should fail open and remain capability-narrowed. |
Start from this manifest shape and adapt versions, scripts, and the package manager to your own repository:
{
"name": "@your-scope/alisio-plugin-example",
"version": "0.1.0",
"description": "A concise description of the plugin.",
"keywords": ["alisio-plugin"],
"license": "MIT",
"repository": { "type": "git", "url": "https://github.com/OWNER/REPOSITORY.git" },
"homepage": "https://github.com/OWNER/REPOSITORY#readme",
"bugs": { "url": "https://github.com/OWNER/REPOSITORY/issues" },
"type": "module",
"engines": { "node": ">=22.16" },
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
"files": ["dist", "README.md", "LICENSE", "cover.svg", "icon.svg"],
"peerDependencies": { "@alisio/sdk": ">=0.1.0-alpha.10 <0.2.0" },
"devDependencies": { "@alisio/sdk": "0.1.0-alpha.10" }
}Third-party release checklist
- [ ]
npm pack --dry-runshowsdist/index.js,dist/index.d.ts,README.md,LICENSE, and registered resources. - [ ] Your repository's lint, type, test, and build checks pass; scan the packed tarball for credentials and local paths.
- [ ] The published package has the
alisio-pluginkeyword and an entry Alisio can resolve. - [ ] Publish with your authorized npm workflow; this monorepo's Changesets and
publish-*scripts are first-party only. - [ ] Verify the registry result:
npm view <package> version. - [ ] Verify the consumer path:
alisio install npm:<package>@<version>, thenalisio plugins doctor. - [ ] If applicable, verify configured credentials stay in Alisio/the environment rather than the package, and document the minimum compatible Alisio runtime.
Catalog inclusion
This catalog is not npm discovery. Local packages under packages/* are discovered automatically; an external package appears only after a maintainer accepts a pull request to this repository that adds it to registry/plugins.json. There is no separate issue form or automated submission endpoint in this repository.
After publishing, open a pull request against GustavoGutierrez/alisio-plugins that edits registry/plugins.json and includes the requested review information below. Maintainers run the scanner and commit its generated cache/pages; do not hand-edit generated catalog files.
The registry schema requires only package. Its optional fields are title, npmUrl, repository, homepage, cover, icon, categories, and featured — exactly as defined in registry/plugins.schema.json.
{
"package": "@your-scope/alisio-plugin-example",
"categories": ["tools"],
"cover": "cover.svg"
}| Maintainer review information | How it is used or verified |
|---|---|
| npm package name and published version; install command | package is the sole required registry field. The catalog derives alisio install npm:<package> and reads the latest npm version. Include the version you tested in the pull request. |
| Repository URL, homepage, bugs URL, description, and license | The scanner reads npm metadata; registry repository and homepage can override missing/incorrect packument values. bugs, description, and license have no registry override. |
| Category and capabilities | categories is an optional registry override. Use only: model-provider, methodology-harness, memory, subagents, search, tools, security, analytics, mcp, storage, ui, decisions. Explain actual capabilities in the PR; capabilities are not a schema field. |
| Peer SDK range, Node compatibility, tests and validation evidence | These are maintainer review evidence, not registry fields. Include your @alisio/sdk peer range, engines.node, commands/results, and the exact published version installed with Alisio. |
| Security and integration details | Explain network/process/write behavior, credential handling, durable-data behavior, optional integrations, and any resources. The scanner never imports or executes third-party code. |
| Cover and presentation | cover is optional. It may be a package-relative path such as cover.svg, or a /... path to a file already committed under site/public. Otherwise the scanner checks package.json alisio.cover, then root cover.svg, .png, .jpg, .jpeg, .webp. Prefer a 16:9, 1600×900 SVG under 512 KB and ensure it is included in the npm tarball. An optional square icon follows the same path rules, then package.json alisio.icon, falling back to a root icon.svg, .png or .webp. See Cover and icon artwork. |
npmUrl, title, and featured are optional presentation overrides, not evidence substitutes. The scanner fetches npm metadata and, only to find a cover, reads the published tarball without executing it. A maintainer can reject or defer a registry change when the package, metadata, safety, or validation evidence is insufficient.
Cover and icon artwork
Every plugin can ship two images: a wide cover for the catalog card and detail page, and a square icon for compact lists, the catalog JSON and host UIs. Both are optional, but first-party packages ship both. Put them at the package root as cover.svg and icon.svg and list both in package.json files, so the scanner finds them locally and in the published tarball. Also declare the icon in package.json so Alisio (>= 0.11.2) can show it for an installed plugin even when no catalog entry is available:
{
"files": ["dist", "README.md", "LICENSE", "cover.svg", "icon.svg"],
"alisio": { "icon": "icon.svg" }
}alisio.icon must be an image path inside the package; the host rejects files over 256 KB. The catalog scanner honors the same field, after a registry icon and before the root icon.* convention.
| Property | Cover | Icon |
|---|---|---|
| Aspect ratio | 16:9 | 1:1 (square) |
| Recommended size | 1600 × 900 | 512 × 512 |
| Minimum raster size | 640 × 360 | 128 × 128 |
SVG viewBox | 0 0 1600 900 | 0 0 512 512 |
| Formats, in lookup order | svg, png, jpg, jpeg, webp | svg, png, webp |
| Maximum file size | 512 KB | 64 KB |
| Default file name | cover.<ext> | icon.<ext> |
| Missing file | Default cover is used | icon: null; no fallback |
| Published to the site at | /covers/<slug>.<ext> | /icons/<slug>.<ext> |
Design rules for icons:
- Bake the background into the file: a full-bleed rounded square (
rx≈ 112 on a 512 viewBox). Hosts show the icon as-is, on light and dark themes. - Use one bold glyph that stays legible at 32 px. Avoid text, hairline strokes and fine detail.
- Derive colors and motif from the plugin's cover so both read as the same brand.
- Keep SVGs self-contained: no
<image>, scripts, external references or web fonts, and prefix gradient and filter ids with the plugin name so several icons can be inlined on one page. - A plugin that integrates a specific vendor or library (for example OpenAI, Atlassian or Brave) may use that vendor's official mark, unmodified in shape and proportions and in the colors its brand guidelines allow, on the icon's background. Record the mark owner and source in an XML comment at the top of the file. Never imply endorsement beyond the integration itself.
Catalog JSON
Every site build publishes a minified, machine-readable catalog at https://gustavogutierrez.github.io/alisio-plugins/catalog.json. The Alisio desktop app reads it to list, search, filter and install plugins without the CLI. It is derived from the committed cache, so it changes only when the cache is refreshed. Image URLs are built from the site URL, defined once in scripts/lib/catalog.mjs; set ALISIO_SITE_URL (an absolute https URL) at build time to publish under another domain.
The top level carries schemaVersion (1), generatedAt (the newest publishedAt, or null), site, categories ({ id, count } for each known category in use) and plugins, sorted by slug. Each plugin carries:
| Field | Meaning |
|---|---|
id, slug, name | Plugin id (or null), URL slug, display name. |
package, version | npm package to install and its catalog version. |
description | One-line summary. |
categories, tags | Filter facets: known category ids, plus keywords without alisio-plugin or category ids. |
cover, icon | Absolute image URLs, or null. |
sdk | The @alisio/sdk peer range, or null. |
author, license, homepage, npmUrl, repository | Display metadata; links are http(s) or null. |
publishedAt, featured, source | Publish time, promotion flag, local or third-party. |
First-party quick path
corepack enable
pnpm install
pnpm check # lint, types, tests, build, pack check
pnpm diagrams:check # committed SVGs are newer than their sourcesThis is the first-party-only path. Scaffold under packages/<name>/ following the conventions in the root README and CONTRIBUTING.md.
The contract
A plugin is an ES module whose default export is a Plugin object. Use the SDK's definePlugin helper (an identity function that only adds typing) and textResult to build tool results.
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!");
},
});
},
});The host validates and enforces this contract:
| Member | Required | Rule |
|---|---|---|
id | yes | Matches ^[a-z0-9][a-z0-9.-]{0,63}$, e.g. acme.hello. |
version | yes | Strict semver: ^\d+\.\d+\.\d+(?:-[\w.-]+)?$ (a pre-release suffix is allowed; build metadata is not). |
apiVersion | yes | Exactly 1. |
setup(api) | yes | A function; may be async. Registers everything. |
name, description | no | Non-empty human-friendly catalog text used by /plugins. |
categories | no | Optional catalog categories: model-provider or methodology-harness. The host validates the value strictly (see below) and also derives model-provider from provider registrations. |
extensions | no | Declarative extension providers, registered at priority 0 before setup runs. |
dispose() | no | Releases resources when Alisio closes. |
What the host guarantees:
- Duplicate ids are rejected (
Duplicate plugin: <id>). - Contract validation runs before
setup. An invalid id, version, orapiVersionfails the load. setupfailures roll back every registration, then calldispose(). Everyregister/oncall returns an unregister function; the host reverses all of them on failure and then awaitsdispose()before rethrowing.- Everything a plugin registers is removed automatically when it unloads.
Catalog categories and host compatibility
categories is one of the few fields the host validates against a closed set (the SDK's PluginCategory). Validation is strict: a host that does not know a declared value fails the load with a contract error instead of ignoring the unknown value. The known values are model-provider (the original value) and methodology-harness (@alisio/sdk 0.1.0-alpha.10 / @alisio/core0.1.0-alpha.15 and newer).
A plugin cannot express a host-version dependency. Plugins depend on @alisio/sdk only, never on @alisio/core, and the SDK peer range is not a host version the host enforces. So declaring a category that a newer SDK introduced fails the load on older hosts, and no manifest field prevents it. Document the minimum Alisio runtime in your README and give the upgrade command (npm install -g @alisio/alisio-code@<version>, or pnpm add -g / bun add -g).
Capability surface
Every namespace a plugin receives on api, what it gives you, how names are scoped, and how the effect/permission model applies.
| Namespace | What it gives you | Naming / namespacing | Effect and permissions |
|---|---|---|---|
api.tools.register(tool) | Tools the model can call. | External plugins get p_<10-hex>_<name> (10 hex chars of the SHA-256 of the plugin id); built-ins stay unprefixed. Must match ^[a-zA-Z0-9_-]{1,64}$ after prefixing. | Carries effect (read/write/process/external/internal). read is always allowed; write/process/external are gated by policy or approval. |
api.commands.register(name, handler, options?) | Slash commands. | External: <plugin id>:<name> (e.g. /acme.hello:name or /command acme.hello:name); built-ins stay unprefixed. Duplicate names throw. | Not effect-gated; still runs in-process with full privileges. |
api.events.on(handler) | Observe versioned RunEvents (schemaVersion, runId, sessionId, seq, type, timestamp, data). | Per plugin. | Read-only; a throwing observer cannot break the run. |
api.context.register(provider) | () => Promise<string> text added to the model context. | Per plugin. | Not gated. |
api.resources.skills(path) / .prompts(path) / .agents(path) | Register resource directories relative to the plugin file. | Directories are tagged with the plugin id. | Not gated. Skills and prompt templates are visible to the host in the same boot; agent definitions are not (see hard limits). |
api.resources.list(kind) | The directories every plugin registered for skills, prompts or agents, with the owner plugin id. | — | Read-only view. |
api.state.get(key) / .set(key, value) | Small JSON state per plugin, persisted in the session database. | Scoped to the plugin id. | Not gated. |
api.storage.sqlite(path) | A private (0600) SQLite file, creating parent directories (0700). Returns the synchronous SqlDatabase port (exec, prepare, transaction, close; prepared statements cached by SQL text; FTS5 available). | Caller-supplied path. | Not gated; the database is closed when the plugin unloads unless the plugin closed it first. |
api.compaction.register({ beforeCompact, afterCompact }) | Contribute instructions, output fields, injected context and a report to core compaction. | Per plugin. | Host timeout 15 s; a failure or timeout is recorded as a plugin failure and the core continues. |
api.session.onStart(handler) | Text injected once at the start of a new, empty session (persisted). | Per plugin. | Host timeout 15 s; failures isolated. |
api.session.onEnd(handler) | Called when an interactive session ends (/clear, /exit, quit). | Per plugin. | Host timeout 10 s; handlers run concurrently; failures isolated. |
api.model.complete(request) | Provider-agnostic text completion. Plugins never import provider SDKs. | Per plugin. | Hard 120 s timeout; does not count against limits.maxTokens. |
api.models.list(signal?) / .resolve(reference, signal?) | Credential-free access to configured /connect models; resolves canonical provider/model or a unique bare id. | Per plugin. | Not gated. |
api.providers.register(provider) | Add selectable model-provider metadata and a factory. Registrations coexist; /connect selects one. | Unique by provider id. | Not gated by effect; provider credentials stay in the host. |
api.sessions.* (spawn, create, run, get, children, ancestors, cancel, enqueue, isRunning, capabilities, model, workspace, setStatus) | Child sessions: separate persisted conversations with fresh context and narrowed permissions. | — | A child can never exceed its parent; aborting a parent aborts running descendants. |
api.ui.status(key, text, detail?) / .panel(id, provider) / .select(req) / .askQuestions(req) / .open(sessionId) / .interactive() | Status text, tree panels, and interactive prompts. | Status keyed plugin:key; panels keyed plugin:id. | Interactive UIs only; headless calls resolve to undefined/false and never hang. |
api.extensions.register(point, provider, options?) | Replace a typed host extension point: mascot, startup-screen, or websearch. | Resolution: highest priority, then plugin id, then registration order. | Not gated by effect; a slow or throwing provider falls back to the default. |
api.apps?.register(app) (Alisio >= 0.11.5) | A full-page plugin app: a sandboxed iframe that replaces the main chat area, listed in the left panel's Apps group, plus typed actions the frame calls. Returns an unregister function. | app.id matches ^[a-z][a-z0-9-]{0,39}$, unique per plugin; action names ^[A-Za-z][A-Za-z0-9_.-]{0,63}$. | Every action declares effect (read/write/process) and an inputSchema; the host validates the input and applies the session's effect gate. From Alisio 0.11.6 an effect the chat would ask about shows the same approval prompt; a denial is not_permitted with the denied effect. See Plugin apps. |
The namespacing rule in one sentence: external plugins are namespaced (tool names prefixed, commands under <plugin id>:) and their internal effect is downgraded to external; built-in plugins are unprefixed and may declare internal.
Plugin apps
api.apps (Alisio >= 0.11.5) lets a plugin open a full page in the host's main area. Use an app when people need a working surface of their own (an editor, a studio, a dashboard); use a tool when the model should act, a command for a one-shot action, and a view for read-only output in the chat.
Feature-detect and state the minimum host. api.apps is optional. Register everything else first and add the app only when it exists; never open a separate UI as a fallback. A plugin that cannot work without its app sets its peer range to @alisio/sdk >=0.11.5 <0.12.0.
export default definePlugin({
id: "acme.notes",
setup(api) {
// Tools, commands and views first, so they work on every host.
if (!api.apps) return; // Explain in /help that the studio needs Alisio >= 0.11.5.
api.apps.register({
id: "notes",
label: { en: "Notes", es: "Notas" },
html: () => renderAppHtml(), // self-contained, <= 8 MiB
actions: {
"notes.list": {
effect: "read",
inputSchema: { type: "object", properties: {}, additionalProperties: false },
handler: async (_input, ctx) => listNotes(ctx.workspace, ctx.signal),
},
},
busy: () => jobs.running(),
dispose: () => jobs.cancelAll(), // synchronous and idempotent
});
},
});Lifecycle. Register in setup and run inference only later, inside handlers. The host removes the app and calls dispose when the plugin closes or setup fails. Keep state per plugin activation, with no global disposers shared between workspaces. Return true from busy while background work runs so the workspace is not evicted.
Actions. The host validates input against inputSchema ($ref, $id and $async are not supported), then applies the same effect gate as tools. An effect the session already grants runs. From Alisio 0.11.6, when the chat would ask, the host shows the same approval prompt (allow once, allow for the session, deny) labelled with the plugin, app and action; "allow for the session" widens the session exactly like the chat. A denial, an unanswered prompt or a read-only session returns { type: "response", id, error: "not_permitted", effect: "write" | "process" }, so tell the user which permission is missing. The host is the only authority: do not re-check api.sessions.capabilities inside a handler, because it reflects standing grants only and would reject an action the user just allowed once. In Alisio 0.11.5 there is no prompt: anything not already granted is rejected. The session comes from the authenticated route, never from the input. Each call times out after 30 s, counted from when the handler starts (not while the approval prompt waits, which is capped at 10 minutes), and aborts ctx.signal: long work returns a job id at once and the frame polls for it. Results must be JSON-serializable, and errors never echo inputs or secrets. Inside handlers use api.models, api.model.complete and api.sessions; reach other plugins only through a child session with an explicit allowed-tools list. Host limits (PLUGIN_APP_LIMITS): 8 MiB HTML, 37 MiB request, 90 MiB result, 8 requests in flight.
The frame. The HTML runs in a sandboxed iframe with an opaque origin (allow-scripts allow-downloads allow-modals allow-popups) under a strict CSP:
| Rule | Consequence |
|---|---|
script-src 'nonce-…' | Every inline <script> carries nonce="__ALISIO_NONCE__"; the host replaces it on each load. |
No allow-forms, form-action 'none' | A <form> submit does nothing; use buttons and Enter-key handlers. |
default-src 'none', connect-src blob: | No fetch, XHR or external script, style or font URLs; inline everything and use data:/blob: for assets. All data goes through actions. |
No 'unsafe-eval' or 'wasm-unsafe-eval' | No eval, new Function or WebAssembly. Workers must be blob: URLs. |
No allow-same-origin | No cookies, no credentials, no access to the parent DOM. |
Transport. On load the host posts { type: "alisio.app.connect", locale, visible } with a MessagePort. Accept only the first one, and only when event.source === window.parent. Send { type: "request", id, action, input?, idempotencyKey? } over the port and match { type: "response", id, result } or { type: "response", id, error }. Error codes: not_found, invalid_input, not_permitted, timeout, failed, too_large, too_many_requests, unauthorized, network. Show an "unavailable" state on not_found (the plugin was disabled or removed).
Locale and visibility. Labels take { en, es } with en as the fallback. Follow the host's { type: "locale" } messages instead of shipping a language selector, and pause polling on { type: "visibility", visible: false }.
Host behavior. Opening an app hides the chat but keeps it mounted, so drafts survive. Frames stay mounted while hidden; each project gets its own instance and session. A full reload loses unsaved text, so persist through actions and warn with beforeunload while edits are pending.
Not available yet: declarative navigation, icons or deep routes, a host jobs API or SSE, model.stream, a browser SDK, and file handles. Disabling a plugin blocks new actions but does not close an open port or cancel running jobs.
Testing. Unit-test the action handlers and the HTML builder: every <script> has the nonce placeholder, there are no external URLs and no <form>. Check the page visually with alisio serve from Alisio 0.11.5 or later.
Hard limits: how far plugins reach
This is the section to read before loading code you did not write.
| Limit | What it means |
|---|---|
| No sandbox. | Plugins run in-process with the user's full privileges. The effect field is availability metadata, not isolation. A plugin manifest or a subprocess is not a sandbox. |
--read-only disables external plugins entirely. | They are not loaded. It also disables writes, processes, network tools and MCP. Built-in plugins are unaffected. When a slash-prompt template requires a capability that --read-only withholds, the CLI prints the exact capability flag (--allow-write / --allow-process) and tells the user to run without --read-only; install is refused outright. |
| Child sessions only narrow. | A child can never gain a capability its parent lacks. A read-only parent poisons all descendants: readOnly is inherited (`parent.readOnly |
External names are rewritten; internal is downgraded. | External tool names get the p_<hash>_ prefix; an external plugin's internal effect becomes external. |
Omitting effect means external, not read. | In the runner, effect defaults to external (t.effect ?? "external"). Only declare read for tools with no side effects. |
paths() is not a write gate. | A tool's paths(input) result feeds context resolution (context.beforePaths), so nested instructions load. It does not restrict what the tool may touch. |
Any external plugin auto-allows the external policy. | When at least one external plugin loads (or any p_-prefixed tool is registered), the runtime external policy turns on. Enabling a plugin is a real widening of what the model may do. |
| Hook timeouts are host-enforced but cannot preempt sync code. | Compaction and session-start hooks: 15 s. Session-end hooks: 10 s. model.complete: a hard 120 s timeout. A timeout aborts the wait and the AbortSignal, but cannot stop blocking synchronous code. |
| Extension renders are bounded. | A provider has a 250 ms rendering budget, 12 lines for a mascot and 60 for a startup-screen. Rendering is synchronous, so a slow provider is replaced by the default after it returns; it cannot be preempted. |
| Duplicate tool or command names throw. | Duplicate tool: <name> and Duplicate command <key>. Tool names must fit ^[a-zA-Z0-9_-]{1,64}$ after prefixing. |
Plugins cannot import another plugin or @alisio/core. | Depend on Node built-ins and @alisio/sdk only. |
| External agent resources are not in the built-in subagents catalog in the same boot. | The built-in subagents plugin reads api.resources.list("agents") during its own setup, which runs before external plugins activate. Skills and prompt templates are collected after all plugins activate, so they are visible. Workaround: execute child sessions directly with api.sessions.create/.run and load the packaged agent/skill files yourself — the reference plugin @alisio/plugin-wayfinder does exactly this. |
| Headless UI calls never hang. | ui.select and ui.askQuestions resolve to undefined (and every question id to undefined); ui.open returns false; ui.interactive() returns false. |
| Installation runs npm with your privileges. | alisio install runs npm install --prefix …, which may run the package's lifecycle scripts. The agent-facing plugin_install tool uses the process effect and is unavailable under --read-only. |
The most likely mistakes
| Symptom | Cause | Fix |
|---|---|---|
Error: Child sessions are not available yet / Model resolution is not available yet / Model completion is not available yet | api.sessions.*, api.models.* and api.model.complete are bound after plugins activate. Calling them directly inside setup() throws. | Defer those calls to tool execution, a command handler, or a lifecycle hook — anything that runs after startup. |
Capability denied: external on a tool that obviously just reads | You omitted effect, so it defaulted to external, and the external policy is off. | Declare effect: "read" for side-effect-free tools. |
Duplicate tool: … / Duplicate command … at load | Two registrations collide, or an external tool name exceeds 64 chars after the p_<hash>_ prefix. | Uniquify names; keep the prefixed length within 64 characters. |
| Tool works locally, is missing after install | The package lacks "keywords": ["alisio-plugin"], or exports["."]/main/./index.js does not resolve to the built entry. | Add the keyword and ship dist with a resolvable entry. |
| Agent definitions never show up in the subagents catalog | External agent resources are not visible in the same boot (see hard limits). | Register them for catalog interoperability, but run child sessions directly and load the files yourself. |
| A hook appears to run twice / state is duplicated | setup is called once per activation; re-registering on reload is expected. Everything is unregistered on unload. | Register idempotently or rely on the unload cleanup. |
| Diagram check fails on CI with no changes | diagrams:check compares mtimes, not hashes; a fresh checkout can make all files share a timestamp. | It is a local authoring aid, not a CI gate. Re-render with pnpm diagrams. |
Package shape and publishing
| Requirement | Detail |
|---|---|
| Package name | First-party only: @alisio/plugin-*. For an external package, use a distinct npm name (for example @your-scope/alisio-plugin-*); the host and registry do not require that pattern. |
| Keyword | alisio-plugin is required; a package without it is rejected, so a typo cannot load an unrelated package. |
| Module format | ESM. Published plugins must ship JavaScript (plus declarations). |
| Entry resolution | exports["."] (import, then node, then default), then main, then ./index.js. |
| Node target | >=22.16. |
alisio-plugin.json | Required when a plugin is a directory given as a path: { "apiVersion": 1, "entry": "./index.js" }, and the entry must stay inside the directory. Optional for an npm package (a package may ship one too). |
| Dependencies | Node built-ins and @alisio/sdk only. @alisio/sdk stays in both peerDependencies and devDependencies. Never import another plugin or @alisio/core. |
| Shipped files | dist JavaScript and declarations, README, MIT LICENSE, and every registered resource (e.g. .agents, assets). |
| Versioning here | First-party only: Changesets. Bump one package with pnpm bump-one -- <name> <patch|minor|major> --summary "<text>", or run pnpm changeset then pnpm run version (which also runs scripts/sync-versions.mjs). pnpm publish-one -- <name> and pnpm publish-all are dry runs; add --publish for an intentional publish. |
No first-party version is published without explicit authorization and npm authentication. This repository stores no secret; see SECURITY.md.
The first-party publish tooling runs a preflight per package: npm authentication (npm login or NPM_TOKEN), a clean committed tree, matching package.json/src/version.ts, a packed tarball free of local machine paths and credentials (pnpm pack:check, which runs the leak scanner over each tarball), and a version not already on the registry. Dist-tags default to latest, or next for prerelease versions. pnpm release:changesets is the Changesets-native, tag-creating flow used by CI; pnpm publish-all is the explicit, preflighted flow.
First-party published packages carry no machine-specific paths and no credentials. The same guarantee is enforced on every pull request by pnpm check: leak:check scans every tracked file and pack:check scans each packed tarball, so both surfaces are covered before a merge.
Installing and smoke-testing
alisio install installs an npm package into Alisio's global plugins directory and records the package name (never the resolved path) in the global config's plugins array.
alisio install npm:@alisio/plugin-x # latest
alisio install npm:@alisio/plugin-x@1.2.3 # pinned
alisio install @alisio/plugin-x # bare name: same as npm:
alisio install npm:@alisio/plugin-x --update # refresh to @latest, keep the name
alisio install npm:@alisio/plugin-x -y # skip the pre-install confirmation
alisio install npm:@alisio/plugin-x --trust-plugin # same as --yes| Aspect | Behavior |
|---|---|
| Accepted specs | npm:<package>[@<version>], or a bare package name. Names allow letters, digits, ., _, -, plus / for scoped names and @ for a version. |
| Rejected specs | Unknown prefixes (git:, file:, registry:, URLs), absolute or .. paths, and shell metacharacters. A failed install never reruns npm silently. |
| Where it lands | <config home>/plugins/node_modules/<package>; the name is added to <config home>/config.json (atomic write, unrelated fields preserved, no duplicates). |
| Privileges | npm install may run the package's lifecycle scripts with your user privileges. On an interactive terminal Alisio warns and asks for confirmation; headless runs must pass --yes/--trust-plugin or they fail before npm runs. |
Under --read-only | Installing is refused and the plugin never loads. |
To smoke-test a local build:
alisio --plugin ./dist/index.js # explicit trust for this entry
alisio plugins list # installed/known plugins
alisio plugins doctor --plugin ./dist/index.js # tools, commands, extensions, built-insalisio plugins install, alisio plugins remove and alisio plugins validate do not exist. Installing is alisio install; removal is editing the global config and the plugins directory; validation happens at load, surfaced by alisio plugins doctor.
SDK version pinning
The published @alisio/sdk is 0.1.0-alpha.10 as of this writing. Verify the current version before you pin:
npm view @alisio/sdk versionThe upstream docs example pins ^0.1.0-alpha.16, which is stale and ahead of what is published. Prefer a range this repository actually uses:
{
"peerDependencies": { "@alisio/sdk": ">=0.1.0-alpha.10 <0.2.0" },
"devDependencies": { "@alisio/sdk": "0.1.0-alpha.10" }
}Testing in this repository
- Unit tests are required for every plugin package.
- Cover at least one happy path and one unhappy path per functional scenario (positive, and negative/error/boundary/valid-alternative).
- Strict TDD is an optional, explicitly recorded project decision, not a default. Recording it is the reference plugin's convention, not a repository precondition; a coverage-first workflow is acceptable when the decision is written down.
- Tests run as part of
pnpm check(pnpm -r test), so a failing test blocks review.
Documentation caveats
Where the upstream docs and the source disagree, this guide follows the source:
- SDK pin example is stale. The upstream docs show
^0.1.0-alpha.16; the published version is0.1.0-alpha.10(check withnpm view @alisio/sdk version). extensions.registercovers more than the API table says. ThePluginAPItable lists(mascot, startup-screen), but the SDK's typedExtensionPointsmap also includeswebsearch, and the extension points table documents it. Follow the SDK type.ui.askQuestionsis missing from the upstream API table. It exists in the SDK (PluginAPI.ui) and resolves every question id toundefinedwhen no interactive UI is available.setuprollback also callsdispose. The upstream "Plugin object" text says partial registrations are rolled back; the host first reverses every registration and then callsdispose()before rethrowing.
This guide and its Spanish mirror are kept in sync and must be updated together. The two versions must always describe the same behaviour.
