Plugins
Plugin Reference
This is the authoritative reference for the plugin.json manifest format and the Nebo plugin system. A plugin is a managed native binary that multiple skills share.
Manifest Schema
The plugin.json file is stored at <data_dir>/nebo/plugins/<slug>/<version>/plugin.json. All fields use camelCase.
Core Fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id |
string | Yes | -- | NeboLoop artifact ID. Assigned when you create the plugin |
slug |
string | Yes | -- | URL-safe identifier. Must match what skills reference in plugins[].name. Kebab-case |
name |
string | Yes | -- | Human-readable display name |
version |
string | Yes | -- | Semver version string (e.g., "1.2.3") |
description |
string | No | "" |
Brief description of what the plugin does |
author |
string | No | "" |
Publisher name |
platforms |
object | Yes | -- | Map of platform key to PlatformBinary. See Platform Binaries below |
signingKeyId |
string | No | "" |
ED25519 signing key ID used to sign binaries |
envVar |
string | No | "" |
Custom environment variable name. If empty, defaults to {SLUG}_BIN |
auth |
object | No | null |
Authentication configuration. See Authentication below |
events |
array | No | null |
Event declarations. See Events below |
Platform Binaries
The platforms field maps platform keys to binary metadata. Valid keys:
| Key | OS | Architecture |
|---|---|---|
darwin-arm64 |
macOS | Apple Silicon |
darwin-amd64 |
macOS | Intel |
linux-arm64 |
Linux | ARM64 |
linux-amd64 |
Linux | x86_64 |
windows-arm64 |
Windows | ARM64 |
windows-amd64 |
Windows | x86_64 |
Each platform entry has these fields:
| Field | Type | Description |
|---|---|---|
binaryName |
string | Filename of the binary (e.g., "gws", "gws.exe") |
sha256 |
string | SHA256 hex hash for integrity verification |
signature |
string | ED25519 signature (base64 encoded) |
size |
number | File size in bytes |
downloadUrl |
string | URL to download the binary (CDN or API path) |
Environment Variables
Plugin binaries are injected into skill scripts as environment variables.
Naming Convention
{SLUG}_BIN
The slug is uppercased and hyphens become underscores:
| Plugin Slug | Env Var |
|---|---|
gws |
GWS_BIN |
ffmpeg |
FFMPEG_BIN |
my-tool |
MY_TOOL_BIN |
nebo-office |
NEBO_OFFICE_BIN |
If envVar is set in the manifest, the custom name is used instead.
Authentication
Plugins that require user credentials declare an auth object. Nebo provides a built-in auth UI with WebSocket-driven OAuth flow.
Auth Schema
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
type |
string | Yes | -- | Auth type identifier (e.g., "oauth_cli") |
label |
string | Yes | -- | Human-readable label for UI button (e.g., "Google Account") |
description |
string | No | "" |
Description shown during auth step |
commands |
object | Yes | -- | CLI subcommands for auth lifecycle |
env |
object | No | {} |
Environment variables injected when running auth commands |
Auth Commands
| Field | Type | Required | Description |
|---|---|---|---|
commands.login |
string | Yes | CLI args for login. Appended to binary path (e.g., "auth login") |
commands.status |
string | No | CLI args for status check. Exit code 0 = authenticated. May output JSON |
commands.logout |
string | No | CLI args to clear credentials (e.g., "auth logout") |
Auth Flow
- Status check: Nebo runs
<binary> <status_cmd>on startup. Exit code 0 = authenticated - Login: User clicks "Connect". Nebo spawns
<binary> <login_cmd>in background - OAuth URL: Nebo scans stderr/stdout for HTTP(S) URLs, opens them in the browser
- Completion: Process exits with code 0 = success. Nebo broadcasts
plugin_auth_complete - Logout:
<binary> <logout_cmd>runs synchronously
Auth Example
{
"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": "123456.apps.googleusercontent.com",
"GOOGLE_CLIENT_SECRET": "secret"
}
}
}
Auth HTTP Endpoints
| Endpoint | Method | Description | Response |
|---|---|---|---|
/api/v1/plugins/{slug}/auth/status |
GET | Run status command | { "authenticated": bool, ...plugin_data } |
/api/v1/plugins/{slug}/auth/login |
POST | Spawn login command | { "started": true } |
/api/v1/plugins/{slug}/auth/logout |
POST | Run logout command | { "success": true } |
Auth WebSocket Events
| Event | Payload | Description |
|---|---|---|
plugin_auth_started |
{ plugin, label } |
Login command spawned |
plugin_auth_url |
{ plugin, url } |
OAuth URL discovered in process output |
plugin_auth_complete |
{ plugin } |
Login succeeded (exit code 0) |
plugin_auth_error |
{ plugin, error } |
Login failed or errored |
Events
Plugins can declare event-producing capabilities. A long-running watch process outputs NDJSON to stdout, and Nebo auto-emits each line into the EventBus. Other agents subscribe by event name without knowing plugin internals.
Event Schema
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string | Yes | -- | Event name. Prefixed with plugin slug at runtime (e.g., "email.new" becomes "gws.email.new") |
description |
string | No | "" |
Human-readable description of what triggers this event |
command |
string | Yes | -- | CLI args for the watch process. Supports {{key}} template substitution |
multiplexed |
bool | No | false |
If true, NDJSON lines may contain an "event" field for multiplexing |
Event Example
{
"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
}
]
}
NDJSON Protocol
Your watch command must output one JSON object per line to stdout.
Single event type (multiplexed: false):
Every line becomes the payload, emitted under the declared event source.
{"messageId": "123", "from": "alice@example.com", "subject": "Hello"}
Nebo emits: Event { source: "gws.email.new", payload: <entire line> }
Multiplexed (multiplexed: true):
Lines may contain an "event" field that selects the event type. The field is stripped from the payload.
{"event": "email.new", "messageId": "123", "from": "alice@example.com"}
{"event": "email.read", "messageId": "456"}
Nebo emits:
gws.email.newwith payload{"messageId": "123", "from": "alice@example.com"}gws.email.readwith payload{"messageId": "456"}
Lines without an event field fall back to the declared event name.
Template Substitution
Event commands support {{key}} placeholders. These are replaced with values from the agent's input_values at runtime.
"command": "gmail +watch --format ndjson --project {{gcp_project}}"
If the agent has input_values: { "gcp_project": "my-project-123" }, the resolved command becomes gmail +watch --format ndjson --project my-project-123.
Agent Watch Trigger Integration
Agents consume plugin events via watch triggers in agent.json:
{
"email-watcher": {
"trigger": {
"type": "watch",
"plugin": "gws",
"event": "email.new",
"restart_delay_secs": 5
},
"description": "React to new emails",
"activities": [
{
"id": "triage",
"intent": "Triage the incoming email",
"steps": ["Classify urgency", "Draft response if needed"]
}
]
}
}
When event is set:
- The command is resolved from the plugin manifest (no need to hardcode it)
- NDJSON output auto-emits into the EventBus as
gws.email.new - If activities are defined, the inline workflow also runs (dual mode)
- Watches without activities are valid -- they only auto-emit into the EventBus
Event Discovery Endpoints
| Endpoint | Method | Description |
|---|---|---|
/api/v1/plugins/events |
GET | All events across all installed plugins |
/api/v1/plugins/{slug}/events |
GET | Events for a specific plugin |
Response format:
{
"events": [
{
"plugin": "gws",
"name": "email.new",
"source": "gws.email.new",
"description": "Fires when a new email arrives in Gmail",
"multiplexed": false
}
],
"total": 1
}
SKILL.md Plugin Dependency
Skills declare plugin dependencies in their frontmatter:
---
name: gmail
description: Send and read Gmail messages
plugins:
- name: gws
version: ">=1.2.0"
- name: ffmpeg
version: ">=5.0.0"
optional: true
---
Dependency Fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string | Yes | -- | Plugin slug (must match plugin.json's slug field) |
version |
string | No | "*" |
Semver range. See Version Ranges below |
optional |
bool | No | false |
If true, skill loads even if plugin isn't installed |
Version Ranges
| Range | Meaning |
|---|---|
"*" |
Any installed version |
">=1.2.0" |
Version 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 |
Nebo resolves to the highest installed version matching the range.
HTTP Endpoints
Complete list of plugin-related REST endpoints:
| Endpoint | Method | Description | Response |
|---|---|---|---|
/api/v1/plugins |
GET | List installed plugins (deduped, highest version) | { plugins: [...], total } |
/api/v1/plugins/events |
GET | List all plugin events | { events: [...], total } |
/api/v1/plugins/{slug} |
DELETE | Remove plugin and all versions | { message } |
/api/v1/plugins/{slug}/events |
GET | List events for one plugin | { plugin, events: [...], total } |
/api/v1/plugins/{slug}/auth/status |
GET | Check auth status | { authenticated, ...data } |
/api/v1/plugins/{slug}/auth/login |
POST | Start auth login | { started } |
/api/v1/plugins/{slug}/auth/logout |
POST | Clear auth credentials | { success } |
GET /plugins Response
{
"plugins": [
{
"slug": "gws",
"version": "1.2.3",
"name": "Google Workspace CLI",
"description": "Google Workspace integration",
"author": "NeboLoop Inc.",
"hasAuth": true,
"authLabel": "Google Account",
"hasEvents": true,
"eventCount": 2,
"source": "installed"
}
],
"total": 1
}
WebSocket Events
All events broadcast during plugin operations:
| Event | Payload | When |
|---|---|---|
plugin_installing |
{ plugin, platform } |
Before download starts |
plugin_installed |
{ plugin } |
After successful install |
plugin_error |
{ plugin, error } |
Download or verify failure |
plugin_auth_started |
{ plugin, label } |
Auth login begins |
plugin_auth_url |
{ plugin, url } |
OAuth URL discovered |
plugin_auth_complete |
{ plugin } |
Auth login succeeded |
plugin_auth_error |
{ plugin, error } |
Auth login failed |
.napp Archive Format
Plugins can be distributed as .napp sealed archives that bundle the binary with documentation and embedded skills:
my-plugin.napp
├── manifest.json # Package identity: { type, name, version }
├── plugin.json # Full plugin manifest
├── PLUGIN.md # Plugin documentation
├── my-plugin # Native binary (platform-specific)
└── skills/ # Optional embedded skills
├── my-skill-a/
│ └── SKILL.md
└── my-skill-b/
└── SKILL.md
After installation, the archive is extracted to <data_dir>/nebo/plugins/<slug>/<version>/. Embedded skills are discovered by the skill loader automatically.
Storage Layout
<data_dir>/nebo/plugins/
gws/
1.2.3/
manifest.json # Package identity
plugin.json # Plugin manifest
PLUGIN.md # Documentation
gws # Binary (chmod 755)
skills/ # Embedded skills
gws-gmail/
SKILL.md
1.2.2/
... # Older versions coexist
ffmpeg/
5.1.0/
...
Platform-Specific Base Paths
| Platform | Base Path |
|---|---|
| macOS | ~/Library/Application Support/nebo/plugins/ |
| Linux | ~/.local/share/nebo/plugins/ |
| Windows | %APPDATA%\nebo\plugins\ |
Install Codes
Published plugins receive a unique install code:
PLUG-XXXX-XXXX
- Format:
PLUG-prefix + two groups of 4 Crockford Base32 characters - Case-insensitive (Crockford Base32 excludes ambiguous characters I, L, O, U)
- Users paste the code into Nebo to install
- More commonly, plugins install automatically as skill dependencies
Publishing Tool Actions
All plugin management via the NeboLoop MCP plugin tool:
| Action | Description | Requires Developer Account |
|---|---|---|
plugin(action: list) |
List your plugins | No |
plugin(action: get, id: "...") |
Get plugin details | No |
plugin(action: create, name: "...", category: "...") |
Create plugin | No |
plugin(action: update, id: "...") |
Update metadata | No |
plugin(action: delete, id: "...") |
Delete plugin | No |
plugin(action: submit, id: "...", version: "...") |
Submit for review | Yes |
plugin(action: list-binaries, id: "...") |
List uploaded binaries | Yes |
plugin(action: binary-token, id: "...") |
Generate upload token (5min expiry) | Yes |
plugin(action: delete-binary, id: "...") |
Delete a binary | Yes |
Security
- SHA256 verification on every download -- mismatch rejects the binary
- ED25519 signatures verified when signing key is available
- Quarantine on revocation -- binary deleted,
.quarantinedmarker written, dependent skills dropped - Offline resolution -- after first install, everything works without network
- Garbage collection -- unreferenced plugin directories are periodically removed
Complete Example
A full plugin.json with authentication and events:
{
"id": "abc-123",
"slug": "gws",
"name": "Google Workspace CLI",
"version": "1.2.3",
"description": "Google Workspace integration for email, calendar, and drive",
"author": "NeboLoop Inc.",
"platforms": {
"darwin-arm64": {
"binaryName": "gws",
"sha256": "a1b2c3d4e5f6...",
"signature": "base64signature...",
"size": 45678900,
"downloadUrl": "https://cdn.neboloop.com/plugins/gws/1.2.3/darwin-arm64/gws"
},
"linux-amd64": {
"binaryName": "gws",
"sha256": "f6e5d4c3b2a1...",
"signature": "base64signature...",
"size": 42000000,
"downloadUrl": "https://cdn.neboloop.com/plugins/gws/1.2.3/linux-amd64/gws"
}
},
"signingKeyId": "key-001",
"envVar": "",
"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": {}
},
"events": [
{
"name": "email.new",
"description": "Fires when a new email arrives in Gmail",
"command": "gmail +watch --format ndjson",
"multiplexed": false
},
{
"name": "calendar.event",
"description": "Fires on calendar event changes",
"command": "calendar +watch --format ndjson",
"multiplexed": true
}
]
}