/docs / plugins / publishing

Publishing Plugins

This guide covers everything you need to publish a plugin to the Nebo marketplace -- from prerequisites and supported platforms through upload, review, bundling skills, testing, and security.

Prerequisites

  • A NeboAI developer account
  • Your binary compiled for at least one target platform
  • Binary must be a single executable file (no runtime dependencies)

Supported platforms

Platform keyOSArchitecture
darwin-arm64macOSApple Silicon
darwin-amd64macOSIntel
linux-arm64LinuxARM64
linux-amd64Linuxx86_64
windows-arm64WindowsARM64
windows-amd64Windowsx86_64

Publish for as many platforms as you support. At minimum, target darwin-arm64 and linux-amd64.

Step-by-step publishing

1. Select your developer account

developer(resource: account, action: select, id: "<your-dev-account-id>")

2. Create the plugin artifact

plugin(action: create, name: "gws", category: "connectors")

3. Get an upload token (per platform)

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

This returns a curl command with a 5-minute expiry.

4. Upload binary + manifest per platform

Use the returned curl command via the command line, replacing the file path and platform:

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

Repeat for each platform you support.

5. Submit for review

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

Plugin tool actions

ActionDescription
plugin(action: list)List all your plugins
plugin(action: get, id: "...")Get plugin details
plugin(action: create, name: "...", category: "...")Create a new plugin
plugin(action: update, id: "...")Update plugin metadata
plugin(action: delete, id: "...")Delete a plugin
plugin(action: submit, id: "...", version: "...")Submit for review
plugin(action: list-binaries, id: "...")List uploaded binaries
plugin(action: binary-token, id: "...")Generate upload token
plugin(action: delete-binary, id: "...")Delete a binary

Install codes

After your plugin is approved, NeboAI assigns a PLUG-XXXX-XXXX install code. Users can paste this code into Nebo's chat to install the plugin directly. However, plugins are typically installed as dependencies of skills -- users rarely install plugins standalone.

Bundling skills with your plugin

Plugins can embed skills inside a .napp archive. This is the preferred distribution method -- when the plugin installs, its skills install automatically.

.napp archive structure

gws.napp
+-- manifest.json       # Package identity
+-- plugin.json         # PluginManifest (the full manifest)
+-- PLUGIN.md           # Plugin documentation
+-- gws                 # Native binary (platform-specific)
+-- skills/             # Embedded skills (optional)
    +-- gws-gmail/
    |   +-- SKILL.md
    +-- gws-calendar/
    |   +-- SKILL.md
    +-- gws-drive/
        +-- SKILL.md

After installation, embedded skills are discovered by the skill loader automatically. They appear as Tier 2.5 -- between marketplace-installed skills and user skills.

Directory structure on disk

What the user sees on disk after install (under the platform-native Nebo data directory — ~/Library/Application Support/Nebo/ on macOS, %APPDATA%\Nebo\ on Windows, ~/.local/share/nebo/ on Linux; NEBO_HOME overrides):

<data_dir>/
  nebo/plugins/                          # CODE -- replaceable on upgrade
    gws/
      1.2.3/
        manifest.json                    # Package identity
        plugin.json                      # Cached PluginManifest
        PLUGIN.md                        # Documentation
        gws                              # Your binary (chmod 755)
        skills/                          # Embedded skills (if bundled)
          gws-gmail/
            SKILL.md
          gws-calendar/
            SKILL.md

  appdata/plugins/                       # DATA -- never touched by updates
    gws/
      sidecar.log                        # Process output
      cache.db                           # Your plugin's persistent data
      credentials.json                   # Whatever your plugin stores

Code and data are physically separated. The update system operates on nebo/plugins/ but never touches appdata/plugins/. Your plugin's databases, caches, and user files survive all version upgrades and reinstalls -- same model as iOS apps. Multiple versions can coexist. Each skill resolves to the highest installed version matching its semver range.

Security

  • SHA256 verification: Every binary is hashed on upload. On download, the hash is verified before the binary is written to disk. Any mismatch = download rejected.
  • ED25519 signatures: Binaries are signed with NeboAI's ED25519 key. Signatures are verified on download when the signing key is available.
  • Quarantine: If a plugin is revoked (security issue, policy violation), Nebo deletes the binary and writes a .quarantined marker. The plugin becomes unresolvable, and any skills depending on it are dropped from the loaded set.
  • No network required after install: Once downloaded, resolve() is fully local. Works offline.

Versioning and updates

  • Multiple versions of the same plugin can coexist on disk (e.g., gws/1.2.0/ and gws/1.3.0/)
  • Each skill resolves to the highest installed version matching its semver range
  • When you publish a new version, users get it automatically the next time Nebo resolves the dependency
  • Old versions are cleaned up by garbage collection when no skill references them

Garbage collection

Nebo periodically checks which plugin slugs are referenced by loaded skills. Unreferenced plugin directories are removed. This is deferred (not eager) -- plugins aren't deleted the moment a skill is uninstalled.

Testing during development

During development, you can manually place a plugin binary in the expected directory structure:

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

Then create a skill with plugins: [{name: "my-plugin", version: "*"}] in your user/skills/ directory. The loader will resolve the plugin locally without contacting NeboAI.

Testing authentication

To test auth locally, create a plugin.json in the version directory with your auth config:

{
  "id": "my-plugin",
  "slug": "my-plugin",
  "name": "My Plugin",
  "version": "0.1.0",
  "platforms": {},
  "auth": {
    "type": "oauth_cli",
    "label": "My Service",
    "description": "Connect to My Service",
    "commands": {
      "login": "auth login",
      "status": "auth status",
      "logout": "auth logout"
    },
    "env": {}
  }
}

The auth endpoints (/api/v1/plugins/{slug}/auth/*) read from this cached manifest.

Testing events

Declare events in your local plugin.json:

{
  "id": "my-plugin",
  "slug": "my-plugin",
  "name": "My Plugin",
  "version": "0.1.0",
  "platforms": {},
  "events": [
    {
      "name": "item.created",
      "description": "Fires when a new item is created",
      "command": "watch --format ndjson"
    }
  ]
}

Then create an agent with a watch trigger referencing event: "item.created". The watch process should output NDJSON to stdout.

Manifest validation rules

Nebo validates plugin.json during installation. Invalid manifests are rejected before the binary is written to disk.

FieldRule
slugRequired. Lowercase alphanumeric + hyphens only. No leading/trailing hyphens. Max 64 characters.
versionRequired. Must be valid semver (e.g., "1.2.3", not "latest").
platformsAt least one platform entry required.
binaryNameNo path separators (/, \). No ... Cannot be empty.
auth.commands.loginMust be non-empty if auth is present.
events[].nameMust be non-empty. No path separators.
events[].commandMust be non-empty.

Common validation errors

"slug is required"
"slug 'My Plugin!' contains invalid characters -- use lowercase alphanumeric and hyphens"
"slug must not start or end with a hyphen"
"version 'latest' is not valid semver"
"platforms must have at least one entry"
"binary_name '../evil' contains path separator"
"auth.commands.login must be non-empty when auth is present"

Plugin runtime environment

Plugins are spawned on-demand -- each tool call, hook invocation, or command execution spawns a fresh process with the CLI arguments from the capability definition. There is no persistent plugin process between invocations. The exception is watch triggers, which spawn a long-running process that emits NDJSON events continuously.

Each invocation runs in a sandboxed environment with a controlled set of environment variables.

Environment variables

VariableExampleDescription
NEBO_APP_IDgwsPlugin identifier (from manifest)
NEBO_APP_NAMEGoogle Workspace CLIDisplay name
NEBO_APP_VERSION1.2.3Manifest version
NEBO_APP_DIR<data_dir>/nebo/plugins/gws/1.2.3Plugin binary directory (code -- replaceable)
NEBO_APP_SOCK<data_dir>/nebo/plugins/gws/1.2.3/gws.sockUnix socket path for gRPC
NEBO_APP_DATA<data_dir>/appdata/plugins/gwsPersistent data directory (separate from code -- survives upgrades)
PATHsystem pathStandard path
HOMEuser homeHome directory
TMPDIRtemp directoryTemporary files
LANGlocaleSystem locale
TZtimezoneSystem timezone

All other environment variables (API keys, secrets, database URLs) are stripped -- your plugin must not depend on the user's shell environment.

Data persistence

Store all persistent data in $NEBO_APP_DATA. This directory is physically separated from the code directory -- it lives at <data_dir>/appdata/plugins/<slug>/, not inside the version directory. This means:

  • Upgrading the plugin binary never touches your data
  • Your plugin is responsible for its own schema migrations across versions
  • Data survives reinstalls, version upgrades, and Nebo updates

Common storage patterns:

  • SQLite -- for structured data and queries
  • JSON files -- for simple configuration state
  • File store -- for cached downloads, processed outputs

Blocked variables

These environment variables are always stripped for security:

ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY, JWT_SECRET,
DATABASE_URL, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY,
GITHUB_TOKEN, STRIPE_SECRET_KEY

WebSocket events

Events broadcast during plugin operations:

EventPayloadWhen
plugin_installing{ plugin, platform }Before download starts
plugin_installed{ plugin }After successful install
plugin_error{ plugin, error }On download/verify failure
plugin_auth_started{ plugin, label }Auth login begins
plugin_auth_url{ plugin, url }OAuth URL discovered in output
plugin_auth_complete{ plugin }Auth login succeeded
plugin_auth_error{ plugin, error }Auth login failed

Quick reference

Environment variable naming

{SLUG}_BIN -- slug uppercased, hyphens become underscores.

Install code prefix

PLUG-XXXX-XXXX -- Crockford Base32, case-insensitive.

SKILL.md frontmatter

plugins:
  - name: <slug>          # Required
    version: "<range>"    # Optional, default "*"
    optional: <bool>      # Optional, default false

NeboAI qualified name

@org/plugins/name@version

Same scoping and version resolution rules as skills. See Packaging for details.

Complete plugin.json example

{
  "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/..."
    }
  },
  "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",
      "command": "gmail +watch --format ndjson",
      "multiplexed": false
    },
    {
      "name": "calendar.event",
      "description": "Fires on calendar event changes",
      "command": "calendar +watch --format ndjson",
      "multiplexed": true
    }
  ],
  "dependencies": [
    {
      "name": "ffmpeg",
      "version": ">=5.0.0"
    }
  ],
  "capabilities": {
    "tools": [
      {
        "name": "gws.gmail.triage",
        "description": "Triage Gmail inbox",
        "command": "gmail +triage",
        "inputSchema": {
          "type": "object",
          "properties": {
            "limit": { "type": "integer", "description": "Max emails to process" },
            "label": { "type": "string", "description": "Gmail label to filter" }
          }
        },
        "approval": true,
        "timeoutSeconds": 120
      },
      {
        "name": "gws.calendar.create",
        "description": "Create a Google Calendar event",
        "command": "calendar +create",
        "approval": true,
        "timeoutSeconds": 30
      }
    ],
    "hooks": [
      {
        "hook": "tool.pre_execute",
        "hookType": "filter",
        "priority": 50,
        "command": "hooks tool-pre-execute",
        "timeoutMs": 500
      }
    ],
    "commands": [
      {
        "name": "/gmail",
        "description": "Quick access to Gmail operations",
        "command": "gmail",
        "slash": true
      }
    ]
  }
}