Developers

Plugins

Creating a Plugin

A plugin is a shared native binary that Nebo downloads once and makes available to any skill that declares it as a dependency. If three skills depend on gws (Google Workspace CLI), only one copy of the binary exists on disk.

Skills access the binary through an environment variable (GWS_BIN, FFMPEG_BIN, etc.) — your script calls it like any other CLI tool.

When to Use a Plugin

Pattern Use When Example
Plugin Multiple skills share the same binary, or it's large (>5MB) gws, ffmpeg, nebo-office
Skill with binary One skill bundles its own small binary A custom tool for a single skill

If your binary serves exactly one skill, embed it as a skill binary. If multiple skills share it, publish it as a plugin.

The Plugin Manifest

Every plugin has a plugin.json that describes the binary, its platforms, and optional capabilities. This is the core of your plugin.

{
  "slug": "gws",
  "name": "Google Workspace CLI",
  "version": "1.2.3",
  "description": "Google Workspace integration for email, calendar, and drive",
  "author": "Your Name",
  "platforms": {
    "darwin-arm64": {
      "binaryName": "gws",
      "sha256": "a1b2c3...",
      "signature": "base64...",
      "size": 45678900,
      "downloadUrl": "https://cdn.neboloop.com/..."
    },
    "linux-amd64": {
      "binaryName": "gws",
      "sha256": "d4e5f6...",
      "signature": "base64...",
      "size": 42000000,
      "downloadUrl": "https://cdn.neboloop.com/..."
    }
  },
  "signingKeyId": "",
  "envVar": ""
}

The slug is the identifier skills use to reference your plugin. It must be kebab-case, URL-safe, and unique on NeboLoop.

Supported Platforms

Build your binary for as many platforms as possible. At minimum, target darwin-arm64 (Apple Silicon) and linux-amd64.

Platform 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

Your binary must be a single, self-contained executable with no runtime dependencies.

How Skills Use Your Plugin

Skills declare your plugin as a dependency in their SKILL.md frontmatter:

---
name: gmail
description: Send and read Gmail messages
plugins:
  - name: gws
    version: ">=1.2.0"
---

When the skill is installed, Nebo automatically downloads the gws binary for the user's platform. The skill's scripts receive the binary path via the GWS_BIN environment variable.

Script Examples

Bash:

#!/bin/bash
$GWS_BIN gmail list --limit 10

Python:

#!/usr/bin/env python3
import os, 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:

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);

The env var naming convention is {SLUG}_BIN with the slug uppercased and hyphens replaced by underscores. A plugin with slug my-tool becomes MY_TOOL_BIN.

Adding Authentication

If your plugin requires user credentials (OAuth, API keys, etc.), declare an auth block in the manifest. Nebo handles the entire auth UX -- login button in settings, WebSocket-driven OAuth flow, status checks.

{
  "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": "your-client-id",
      "GOOGLE_CLIENT_SECRET": "your-client-secret"
    }
  }
}

How Auth Works

  1. User clicks "Connect" in Nebo settings
  2. Nebo spawns <your-binary> auth login with the declared env vars
  3. If your binary writes an OAuth URL to stderr or stdout, Nebo opens the browser automatically
  4. When the process exits with code 0, auth is complete
  5. <your-binary> auth status is called to verify (exit 0 = authenticated)

Auth Commands

Command Required Purpose
login Yes Triggers the authentication flow. Can be interactive (opens browser)
status No Checks if user is authenticated. Exit code 0 = yes. Can output JSON with details
logout No Clears stored credentials

The commands are appended to your binary path. So "login": "auth login" becomes <binary-path> auth login.

Declaring Events

If your plugin can watch for external changes (new emails, calendar updates, file changes), declare them in the events array. This lets agents subscribe to your events without hardcoding CLI commands.

{
  "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
    }
  ]
}

The NDJSON Protocol

Your watch command must output one JSON object per line to stdout. Two modes are supported:

Single event type (multiplexed: false, the default):

Each line is emitted as the declared event. The entire JSON line becomes the payload.

{"messageId": "123", "from": "alice@example.com", "subject": "Hello"}
{"messageId": "456", "from": "bob@example.com", "subject": "Meeting"}

Both lines emit as gws.email.new.

Multiplexed (multiplexed: true):

Each line 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"}
{"event": "calendar.updated", "eventId": "789", "title": "Standup"}

Lines emit as gws.email.new, gws.email.read, and gws.calendar.updated respectively. If a line has no event field, the declared event name is used as fallback.

Template Substitution

Event commands support {{key}} placeholders that are replaced with values from the agent's input configuration at runtime. This lets you parameterize watch commands without hardcoding credentials or project IDs.

"command": "gmail +watch --format ndjson --project {{gcp_project}}"

How Agents Consume Events

An agent references your plugin event in its agent.json:

{
  "email-watcher": {
    "trigger": {
      "type": "watch",
      "plugin": "gws",
      "event": "email.new"
    },
    "description": "React to new emails",
    "activities": [...]
  }
}

The agent doesn't need to know the CLI command -- it's resolved from your manifest. Events are also auto-emitted into Nebo's EventBus, so other agents can subscribe to gws.email.new without running their own watch process.

Bundling Skills with Your Plugin

The preferred distribution method is a .napp archive that bundles your binary with embedded skills:

gws.napp
├── manifest.json       # Package identity
├── plugin.json         # Your plugin manifest
├── PLUGIN.md           # Documentation
├── gws                 # The binary
└── skills/             # Skills that use this plugin
    ├── gws-gmail/
    │   └── SKILL.md
    ├── gws-calendar/
    │   └── SKILL.md
    └── gws-drive/
        └── SKILL.md

When a user installs the plugin, all embedded skills are installed automatically. The skill loader discovers them via the skills/ directory.

Publishing to NeboLoop

Prerequisites

  • A NeboLoop developer account
  • Your binary compiled for target platforms
  • A PLUGIN.md documenting your plugin

Step by Step

  1. Select your developer account:

    developer(resource: account, action: select, id: "<your-dev-account-id>")
    
  2. Create the plugin:

    plugin(action: create, name: "my-plugin", category: "connectors")
    
  3. Get an upload token:

    plugin(action: binary-token, id: "<PLUGIN_ID>")
    

    Returns a curl command with a 5-minute expiry.

  4. Upload your binary for each platform:

    curl -X PUT "<upload-url>" \
      -F "binary=@./build/my-plugin-darwin-arm64" \
      -F "platform=darwin-arm64" \
      -F "manifest=@./PLUGIN.md"
    

    Repeat for linux-amd64, windows-amd64, etc.

  5. Submit for review:

    plugin(action: submit, id: "<PLUGIN_ID>", version: "1.0.0")
    

After approval, your plugin receives a PLUG-XXXX-XXXX install code. Users can paste it into Nebo, but more commonly your plugin is installed automatically as a dependency of the skills that use it.

Testing Locally

Place your binary in the expected directory structure without NeboLoop:

macOS:

mkdir -p ~/Library/Application\ Support/nebo/plugins/my-plugin/0.1.0/
cp ./build/my-plugin ~/Library/Application\ Support/nebo/plugins/my-plugin/0.1.0/
chmod 755 ~/Library/Application\ Support/nebo/plugins/my-plugin/0.1.0/my-plugin

Linux:

mkdir -p ~/.local/share/nebo/plugins/my-plugin/0.1.0/
cp ./build/my-plugin ~/.local/share/nebo/plugins/my-plugin/0.1.0/
chmod 755 ~/.local/share/nebo/plugins/my-plugin/0.1.0/my-plugin

Then create a skill in your user/skills/ directory with plugins: [{name: "my-plugin", version: "*"}]. Nebo resolves it locally -- no NeboLoop needed.

To test auth or events locally, add a plugin.json alongside your binary with the relevant auth or events configuration.

Security

Nebo verifies every plugin binary:

  • SHA256 hash checked on download -- any mismatch rejects the binary
  • ED25519 signatures verified when the signing key is available
  • Quarantine on revocation -- binary is deleted, a .quarantined marker is written, and dependent skills are dropped
  • Offline-safe -- after install, everything resolves locally with no network needed

Next Steps