Plugins Overview
A Plugin is a managed native binary that skills depend on. Instead of bundling a heavy binary inside every skill that needs it, publish the binary once as a plugin and let skills declare it as a dependency. Nebo downloads the plugin automatically when a skill that needs it is installed.
When to use a plugin vs. embedded binary
| Pattern | Use when | Example |
|---|---|---|
| Plugin | Multiple skills share the same binary, or the binary is large (>5 MB) | gws, ffmpeg |
| Embedded binary | One skill bundles its own small binary | Custom tool specific to a single skill |
You can use both patterns. A skill can embed a binary and declare plugin dependencies. The embedded binary takes precedence for the skill's own execution; plugin binaries are available to scripts via environment variables.
What a plugin can do
A plugin is a single native binary that can contribute multiple capabilities simultaneously. What makes a plugin a "connector" vs a "provider" vs a "tool plugin" is determined by which capabilities it declares in its manifest. A single plugin can declare all of them.
| Capability | Manifest field | What it does | Runtime |
|---|---|---|---|
| Tools | capabilities.tools[] | Registers typed, schema-validated tools the agent can call | Routed through STRAP PluginTool |
| Hooks | capabilities.hooks[] | Intercepts lifecycle events (tool execution, messages, memory, prompts) | HookDispatcher with circuit breaker |
| Commands | capabilities.commands[] | Registers /slash commands that bypass the LLM and execute directly | Chat dispatch, 30s timeout |
| Routes | capabilities.routes[] | Handles HTTP endpoints (OAuth callbacks, webhooks) | Proxied through catch-all handler |
| Providers | capabilities.providers[] | Registers as an AI model provider (LLM, speech, image) | PluginProvider with NDJSON streaming |
| Config | capabilities.configSchema[] | Declares user-configurable settings rendered as a form in the UI | Values injected as env vars |
| Events | events[] | Produces events via long-running watch processes (NDJSON on stdout) | Auto-emitted into EventBus |
| Auth | auth | Declares an authentication flow (OAuth, API keys) with login/status/logout | HTTP endpoints + WebSocket events |
| Permissions | permissions | Declares env var access, network needs, and max execution timeout | Enforced at exec time |
See Plugin Capabilities for the full reference on each capability type.
How it works
The install flow is fully automatic. When a user installs a skill that needs a plugin, Nebo resolves and downloads the binary without any manual steps.
- Publisher uploads a native binary to NeboAI for each platform
- Publisher creates a skill with
plugins:in SKILL.md frontmatter (or another plugin withdependencies:in plugin.json) - User installs the skill or plugin (via marketplace or install code)
- Nebo detects the plugin dependency and downloads the binary silently
- If the plugin declares its own
dependencies[], those are installed recursively - Binary is stored locally at
<data_dir>/nebo/plugins/<slug>/<version>/(the Nebo data directory is platform-native —~/Library/Application Support/Nebo/on macOS,%APPDATA%\Nebo\on Windows,~/.local/share/nebo/on Linux;NEBO_HOMEoverrides) - Plugin runtime data is stored at
<data_dir>/appdata/plugins/<slug>/-- physically separate from the binary, survives upgrades - Skill scripts access the binary via
$${plugin.SLUG_BIN}template variable - If the plugin declares
capabilities.tools[], typed tools are registered for the agent
User installs skill
-> SKILL.md declares plugins: [{ name: "gws", version: ">=1.2.0" }]
-> Nebo downloads gws binary for current platform
-> Skill script runs with GWS_BIN=/path/to/gwsDeclaring plugin dependencies
Add a plugins field to your SKILL.md frontmatter:
---
name: google-workspace
description: Manage Google Workspace -- Gmail, Calendar, Drive
plugins:
- name: gws
version: ">=1.2.0"
---Dependency fields
| Field | Type | Default | Description |
|---|---|---|---|
name | string | required | Plugin slug (must match the plugin's registered slug in NeboAI) |
version | string | "*" | Semver version range |
optional | bool | false | If true, the skill loads even if this plugin isn't installed |
Version ranges
Version ranges follow semver conventions:
| Range | Meaning |
|---|---|
"*" | Any version |
">=1.2.0" | 1.2.0 or higher |
"^1.0.0" | Compatible with 1.x.x (>=1.0.0, <2.0.0) |
"~1.2.0" | Patch updates only (>=1.2.0, <1.3.0) |
"=1.2.0" | Exact version |
Multiple dependencies
---
name: media-processor
description: Process and convert media files
plugins:
- name: ffmpeg
version: ">=5.0.0"
- name: imagemagick
version: ">=7.0.0"
optional: true
---The skill loads only if all required plugins resolve. Optional plugins are silently skipped if missing.
Alternative: requires block
Plugin manifests can also declare dependencies using a requires block, which supports the same fields:
requires:
plugins:
- name: gws
version: ">=1.2.0"
optional: falseUsing plugin binaries in scripts
Plugin binaries are accessible in two ways depending on context.
In SKILL.md body (template variables)
Use $${plugin.SLUG_BIN} syntax. Nebo expands these at skill activation time. The slug is uppercased with hyphens replaced by underscores.
| Plugin slug | Template variable | Expands to |
|---|---|---|
gws | $${plugin.GWS_BIN} | <data_dir>/nebo/plugins/gws/1.2.3/gws |
ffmpeg | $${plugin.FFMPEG_BIN} | <data_dir>/nebo/plugins/ffmpeg/2.0.0/ffmpeg |
my-tool | $${plugin.MY_TOOL_BIN} | <data_dir>/nebo/plugins/my-tool/1.0.0/my-tool |
In script files (environment variables)
When a script runs, plugin binaries are injected as environment variables using the {SLUG}_BIN naming convention:
| Plugin slug | Environment variable |
|---|---|
gws | GWS_BIN |
ffmpeg | FFMPEG_BIN |
my-tool | MY_TOOL_BIN |
Additionally, NEBO_PLUGIN_DATA is set to <data_dir>/appdata/plugins/<slug>/ -- the persistent data directory for this plugin. Use this for caches, databases, and any state that should survive plugin upgrades.
Python example
#!/usr/bin/env python3 import os import subprocess gws_bin = os.environ["GWS_BIN"] result = subprocess.run([gws_bin, "gmail", "list", "--limit", "10"], capture_output=True, text=True) print(result.stdout)
TypeScript example
import { execSync } from "child_process";
const gwsBin = process.env.GWS_BIN!;
const output = execSync(`$${gwsBin} gmail list --limit 10`, { encoding: "utf-8" });
console.log(output);Shell example
#!/bin/bash $GWS_BIN gmail list --limit 10
The plugin.json manifest
Every plugin has a plugin.json manifest that describes the binary, its platforms, and optional capabilities like authentication and events. This file is stored alongside the binary on disk and cached in memory by the PluginStore.
{
"id": "gws",
"slug": "gws",
"name": "Google Workspace CLI",
"version": "1.2.3",
"description": "Google Workspace integration for email, calendar, and drive",
"author": "NeboAI Inc.",
"platforms": {
"darwin-arm64": {
"binaryName": "gws",
"sha256": "a1b2c3...",
"signature": "base64...",
"size": 45678900,
"downloadUrl": "https://cdn.neboai.com/plugins/gws/1.2.3/darwin-arm64/gws"
},
"linux-amd64": {
"binaryName": "gws",
"sha256": "d4e5f6...",
"signature": "base64...",
"size": 42000000,
"downloadUrl": "https://cdn.neboai.com/plugins/gws/1.2.3/linux-amd64/gws"
}
},
"signingKeyId": "key-001",
"envVar": "",
"auth": { ... },
"events": [ ... ],
"dependencies": [ ... ],
"capabilities": { ... }
}Manifest fields
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique identifier -- use the plugin's slug. Deserialization fails without this field. |
slug | string | Yes | URL-safe slug. Must match what skills reference in plugins[].name |
name | string | Yes | Human-readable display name |
version | string | Yes | Semver version string |
description | string | No | Brief description |
author | string | No | Publisher name |
platforms | object | Yes | Map of platform key to PlatformBinary |
signingKeyId | string | No | ED25519 signing key ID |
envVar | string | No | Custom env var name override. If empty, defaults to {SLUG}_BIN |
auth | object | No | Authentication configuration |
events | array | No | Event declarations |
dependencies | array | No | Plugin-to-plugin dependencies |
capabilities | object | No | Structured capability declarations |
id field is required for all plugins. Without it, PluginManifest deserialization fails and the plugin cannot be resolved. Use the slug as the id (e.g., "id": "gws").PlatformBinary
Each entry in the platforms map describes the binary for one platform:
| Field | Type | Description |
|---|---|---|
binaryName | string | Filename of the binary (e.g., "gws" or "gws.exe") |
sha256 | string | SHA256 hex hash for integrity verification |
signature | string | ED25519 signature (base64) |
size | number | File size in bytes |
downloadUrl | string | CDN URL or API path to download the binary |
Authentication
Plugins that require user credentials (e.g., Google OAuth, API keys) can declare an auth block. Nebo provides HTTP endpoints and WebSocket events to drive the auth flow from the frontend.
{
"auth": {
"type": "oauth_cli",
"label": "Google Account",
"description": "Authenticate with your Google Workspace account.",
"commands": {
"login": "auth login",
"status": "auth status",
"logout": "auth logout"
},
"env": {
"GOOGLE_CLIENT_ID": "...",
"GOOGLE_CLIENT_SECRET": "..."
}
}
}Auth fields
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Auth type identifier (e.g., "oauth_cli") |
label | string | Yes | Button label in UI (e.g., "Google Account") |
description | string | No | Description shown to user during auth step |
commands.login | string | Yes | CLI args appended to binary for login |
commands.status | string | No | CLI args for status check. Exit code 0 = authenticated |
commands.logout | string | No | CLI args to clear credentials |
env | object | No | Environment variables injected when running auth commands |
Auth HTTP endpoints
| Endpoint | Method | Description |
|---|---|---|
/api/v1/plugins | GET | Returns hasAuth and authLabel per plugin |
/api/v1/plugins/{slug}/auth/status | GET | Runs status command. Returns { "authenticated": bool } |
/api/v1/plugins/{slug}/auth/login | POST | Spawns login command in background. Returns { "started": true } |
/api/v1/plugins/{slug}/auth/logout | POST | Runs logout command synchronously |
Auth WebSocket events
| Event | Payload | When |
|---|---|---|
plugin_auth_started | { plugin, label } | Login command spawned |
plugin_auth_url | { plugin, url } | OAuth URL discovered in output |
plugin_auth_complete | { plugin } | Login succeeded (exit code 0) |
plugin_auth_error | { plugin, error } | Login failed |
The login flow is asynchronous. Nebo spawns the plugin's login command, scans stderr/stdout for OAuth URLs, broadcasts them to the frontend via WebSocket, and also attempts to open the URL as a server-side fallback.
Plugin events
Plugins can declare event-producing capabilities. When a long-running watch process outputs NDJSON to stdout, Nebo auto-emits each line into the EventBus. Other agents can subscribe to these events without knowing plugin internals.
{
"events": [
{
"name": "email.new",
"description": "Fires when a new email arrives in Gmail",
"command": "gmail +watch --format ndjson --project {{gcp_project}}"
},
{
"name": "calendar.event",
"description": "Fires on calendar event changes",
"command": "calendar +watch --format ndjson",
"multiplexed": true
}
]
}Event fields
| Field | Type | Default | Description |
|---|---|---|---|
name | string | required | Event name. Prefixed with plugin slug at runtime (e.g., "gws.email.new") |
description | string | "" | Human-readable description of what triggers this event |
command | string | required | CLI args for the watch process. Supports {{key}} template substitution |
multiplexed | bool | false | If true, NDJSON lines may contain an "event" field for multiplexing |
NDJSON protocol
Watch processes output one JSON object per line to stdout.
Single event type (multiplexed: false) -- the entire line becomes the event payload, emitted under the declared event source:
{"messageId": "123", "from": "alice@example.com", "subject": "Hello"}Multiplexed (multiplexed: true) -- each line may contain an "event" field that discriminates the event type. The field is stripped from the payload before emission:
{"event": "email.new", "messageId": "123", "from": "alice@example.com"}
{"event": "email.read", "messageId": "456"}If a multiplexed line has no event field, the declared event name is used as fallback.
Agent watch triggers
Agents consume plugin events by declaring a watch trigger with event and command in agent.json. Always provide both -- event enables EventBus auto-emission, command is the fallback if the event isn't found in the manifest:
{
"email-watcher": {
"trigger": {
"type": "watch",
"plugin": "gws",
"event": "email.new",
"command": "gmail +watch --format ndjson",
"restart_delay_secs": 5
},
"description": "React to new emails",
"activities": [...]
}
}When event is set, the runtime first attempts to resolve the CLI command from the plugin's manifest. If the event is not found, the command field is used instead. Without either, the watcher is silently skipped.
Plugin-to-plugin dependencies
Plugins can depend on other plugins. For example, digest needs ffmpeg for media extraction, and nebo-office may need nebo-pdf for shared rendering. Declare dependencies in your plugin.json:
{
"id": "digest",
"slug": "digest",
"version": "1.2.0",
"dependencies": [
{ "name": "ffmpeg", "version": ">=5.0.0" },
{ "name": "imagemagick", "version": "*", "optional": true }
]
}Each required dependency is resolved and installed recursively (same cascade as skill-to-plugin deps). Your plugin binary receives dependency binaries as environment variables: {DEP_SLUG}_BIN. The dependency cascade uses a visited set for cycle protection -- no infinite loops.