Developer Documentation

Build Apps for Nebo

Give your AI new superpowers. Tools, channels, gateways, UI panels — anything you can imagine, compiled to a native binary.

Overview

What Nebo Is

Nebo is a personal desktop AI companion — an always-running agent that manages your digital life. Think of Nebo the way you think of a smartphone: a powerful platform that becomes transformative through its apps.

On its own, Nebo is a capable agent with memory, scheduling, web browsing, file management, and shell access. But the real unlock is Apps. Just as a smartphone's power comes from its app ecosystem, Nebo's power comes from the apps that extend what it can do.

What Apps Are

Nebo Apps are self-contained, precompiled units of incredible functionality. Each app gives Nebo a new superpower — calendar management, email triage, project tracking, home automation, financial analysis — anything a developer can imagine.

Apps provide incredible power in a very safe manner. Every app runs in a sandbox with deny-by-default permissions. It can only access what its manifest declares and the user approves.

The interface is 100% conversational. Users don't interact with apps through separate screens or dashboards. They talk to Nebo, and Nebo uses the app's tools to get things done. "What's on my calendar tomorrow?" — Nebo calls the calendar app. "Create a meeting with Sarah at 3pm" — Nebo calls the calendar app. The user never leaves the conversation.

Apps can optionally provide a settings UI — a configuration panel that opens in Nebo's built-in browser window. This is where users enter API keys, toggle features, and configure the app. The settings UI is driven by the settings array in the manifest (Nebo renders the form automatically), or the app can provide its own full custom UI via the ui capability.

Apps replace and extend Nebo's built-in capabilities. Nebo ships with basic platform tools (local calendar access, screenshots, etc.). When you install a calendar app, it replaces the built-in calendar tool and becomes a superset — local + cloud + aggregation + availability checking.

Compiled-Only Policy

Nebo enforces a strict compiled-only binary policy. All apps must be native compiled executables.

StatusLanguages
SupportedGo (recommended), Rust, C/C++, Zig
RejectedPython, Node.js, Ruby, Java, .NET, shell scripts

Rationale:

  • AI self-modification prevention — An agent with shell access could modify an interpreted script's behavior at runtime. Compiled binaries are immutable after signing.
  • Signature integrity — Ed25519 signatures cover raw binary bytes. A signed binary cannot be modified without invalidating the signature.
  • Sandbox enforcement — Binary format validation (ELF/Mach-O/PE magic bytes) is a fast, reliable gate.

Quick Start

Install the SDK

Official SDKs handle all gRPC server setup, signal handling, and protocol bridging. You just implement handler interfaces.

Go (recommended):

bash
go get github.com/NeboAI/nebo-sdk-go

Rust (coming soon):

toml
# Coming soon
[dependencies]
nebo-sdk = "0.1"

C/C++ (coming soon):

bash
# Coming soon — Add as a CMake subdirectory or copy headers from sdk/c/include/

App Directory Structure

Apps live in the apps/ subdirectory of your Nebo data directory:

text
~/Library/Application Support/Nebo/apps/   # macOS
~/.config/nebo/apps/                        # Linux
%AppData%\Nebo\apps\                        # Windows

apps/
  com.example.myapp/
    manifest.json       # Required: app metadata
    binary              # Required: executable (or named "app")
    signatures.json     # Required for NeboAI distribution, optional in dev
    TOOL.md             # Required: tool instructions for the agent
    data/               # Auto-created: app's sandboxed storage
    logs/               # Auto-created: stdout.log, stderr.log
    ui/                 # Optional: static UI assets (HTML/CSS/JS)

Minimal App Checklist

  1. Create manifest.json with id, name, version, provides
  2. Write TOOL.md describing how the agent should use your tool
  3. Install the SDK (go get github.com/NeboAI/nebo-sdk-go)
  4. Implement the handler interface for your capability (ToolHandler, ChannelHandler, etc.)
  5. Register the handler and call app.Run()
  6. Build a native binary (go build -o binary .)
  7. Place binary + manifest.json + TOOL.md in apps/<your-app-id>/ directory
  8. Nebo auto-discovers and launches it

Configuration

manifest.json

Every app requires a manifest.json:

json
{
    "id": "com.example.myapp",
    "name": "My App",
    "version": "1.0.0",
    "description": "What this app does",
    "runtime": "local",
    "protocol": "grpc",
    "provides": ["tool:my_tool"],
    "permissions": ["network:outbound"],
    "settings": [
        {
            "key": "api_key",
            "title": "API Key",
            "type": "password",
            "required": true,
            "description": "Your API key for the service",
            "secret": true
        }
    ]
}

Required fields:

FieldDescription
idReverse-domain identifier (e.g., com.mycompany.myapp)
nameHuman-readable name
versionSemver string
providesAt least one capability (see below)

Optional fields:

FieldDefaultDescription
runtime"local""local" or "remote"
protocol"grpc"Only "grpc" is supported
permissions[]What the app needs access to
description""Shown in the UI
settings[]Configurable settings (rendered in Settings UI)
oauth[]OAuth provider requirements (see OAuth section below)

Capabilities

Declare what your app provides:

CapabilitygRPC ServiceDescription
gatewayGatewayServiceLLM model routing
tool:<name>ToolServiceA named tool for the agent
channel:<name>ChannelServiceA messaging channel
commCommServiceInter-agent communication
uiUIServiceCustom UI with HTTP proxy
scheduleScheduleServiceCustom scheduling
visionToolServiceVision processing
browserToolServiceBrowser automation

An app can provide multiple capabilities. For example, an app could provide both a tool and a UI panel.

Permissions

Permissions control what the app can access. Deny by default — if not declared, the app can't use it.

PrefixExamplesDescription
network:network:outbound, network:*Network access
filesystem:filesystem:read, filesystem:writeFile system access
memory:memory:read, memory:writeAgent memory access
session:session:readConversation sessions
tool:tool:shell, tool:fileUse other tools
shell:shell:execShell command execution
channel:channel:send, channel:*Channel operations
comm:comm:send, comm:*Inter-agent comm
model:model:chatAI model access
user:user:tokenReceive user JWT in requests
schedule:schedule:createCron job management
database:database:queryDatabase access
storage:storage:read, storage:writePersistent storage
context:context:readAgent context access
subagent:subagent:spawnSub-agent operations
lane:lane:enqueueLane operations
notification:notification:sendPush notifications
embedding:embedding:searchVector embedding access
skill:skill:invokeSkill invocation
advisor:advisor:consultAdvisor system access
mcp:mcp:connectMCP server access
voice:voice:recordVoice/audio access
browser:browser:navigateBrowser automation
oauth:oauth:google, oauth:*OAuth token access
settings:settings:readSettings access
capability:capability:registerCapability registration

Wildcard permissions are supported: network:* matches any network: permission check.

Settings

Settings appear in the Nebo UI under the app's settings panel. Nebo stores them in the database and sends them to your app via the Configure RPC when they change.

Your settings schema is included in the NeboAI app store listing. Apps with unconfigured required settings show a "Needs Setup" badge in the UI.

TypeDescription
textSingle-line text input
passwordMasked text input
toggleBoolean on/off switch
selectDropdown with predefined options
numberNumeric input
urlURL input with validation
json
{
    "key": "region",
    "title": "Region",
    "type": "select",
    "required": true,
    "default": "us-east-1",
    "options": [
        {"label": "US East", "value": "us-east-1"},
        {"label": "EU West", "value": "eu-west-1"}
    ]
}

OAuth Requirements

Apps can declare OAuth provider requirements. Nebo's OAuth broker handles the entire flow — apps receive tokens automatically via the Configure RPC.

json
{
    "oauth": [
        {
            "provider": "google",
            "scopes": ["https://www.googleapis.com/auth/calendar"]
        }
    ]
}
FieldDescription
providerOAuth provider: "google", "microsoft", "github"
scopesArray of OAuth scopes the app needs

When the user installs the app, Nebo prompts them to authorize the required OAuth scopes. The app receives refreshed tokens via the Configure RPC without handling the OAuth flow itself.

Environment Variables

Your app process receives a sanitized environment. All secrets are stripped. You get:

VariableValue
NEBO_APP_DIRApp's installation directory
NEBO_APP_SOCKPath to the Unix socket to create
NEBO_APP_IDApp ID from manifest
NEBO_APP_NAMEApp name from manifest
NEBO_APP_VERSIONApp version from manifest
NEBO_APP_DATAPath to app's data/ directory
PATHSystem PATH (passthrough)
HOMEUser home directory (passthrough)
TMPDIRTemp directory (passthrough)

Critical: Your binary must create a gRPC server listening on the Unix socket at NEBO_APP_SOCK. Nebo waits up to 10 seconds for this socket to appear.

App Launch Sequence

  1. Nebo reads manifest.json and validates it
  2. Finds binary or app executable in the app directory
  3. Checks for .quarantined marker (refuses to launch quarantined apps)
  4. Revocation check (NeboAI-distributed apps only)
  5. Signature verification (NeboAI-distributed apps only, skipped in dev)
  6. Binary validation (rejects symlinks, scripts, non-executables, oversized files)
  7. Cleans up stale socket from previous run
  8. Creates data/ directory for sandboxed storage
  9. Sets up per-app log files (logs/stdout.log, logs/stderr.log)
  10. Starts binary with sanitized environment and process group isolation
  11. Waits for Unix socket to appear (exponential backoff, max 10 seconds)
  12. Connects via gRPC over the Unix socket
  13. Creates capability-specific gRPC clients based on provides
  14. Runs health check
  15. Registers capabilities with the agent (tools, gateway, comm, etc.)

Tool Apps

Declare "provides": ["tool:my_tool_name"] in your manifest.

The STRAP Pattern

All Nebo tool apps must use the STRAP (Single Tool Resource Action Pattern) for their schema and execution routing. This is the same pattern Nebo uses internally to consolidate 35+ individual tools into 4 domain tools — reducing LLM context overhead by ~80%.

Core idea: Instead of registering multiple tools (get_events, check_availability, suggest_slots), register ONE tool with action (and optionally resource) fields that route to the right handler.

Single-resource example (calendar):

text
calendar(action: "get_events", start: "2025-01-15T09:00:00Z", end: "2025-01-16T00:00:00Z")
calendar(action: "next_event")
calendar(action: "check_availability", start: "...", end: "...")
calendar(action: "suggest_slots", duration_minutes: 30, preferred_time: "morning")

Multi-resource example (project manager):

text
project(resource: "task", action: "create", title: "Ship v2", assignee: "alice")
project(resource: "task", action: "list", status: "open")
project(resource: "milestone", action: "create", name: "Beta", deadline: "2025-03-01")
project(resource: "milestone", action: "list")

Schema rules:

  1. Always include an action field as a required string enum
  2. Add a resource field (required string enum) only if your tool manages multiple distinct resource types
  3. All other fields are action-specific parameters
  4. The action enum description should list all available actions

Why STRAP matters:

  • LLMs learn the action routing pattern once and generalize across all operations
  • Tool definitions consume ~6% of context — STRAP cuts that by 80%
  • New operations are just enum additions, not new tool registrations
  • Works identically across Claude, GPT, Gemini, and local models

Go Example — Calculator Tool

go
package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"

    nebo "github.com/NeboAI/nebo-sdk-go"
)

type Calculator struct{}

func (c *Calculator) Name() string        { return "calculator" }
func (c *Calculator) Description() string { return "Performs arithmetic calculations." }

func (c *Calculator) Schema() json.RawMessage {
    return nebo.NewSchema("add", "subtract", "multiply", "divide").
        Number("a", "First operand", true).
        Number("b", "Second operand", true).
        Build()
}

func (c *Calculator) Execute(_ context.Context, input json.RawMessage) (string, error) {
    var in struct {
        Action string  `json:"action"`
        A      float64 `json:"a"`
        B      float64 `json:"b"`
    }
    if err := json.Unmarshal(input, &in); err != nil {
        return "", fmt.Errorf("invalid input: %w", err)
    }

    var result float64
    switch in.Action {
    case "add":
        result = in.A + in.B
    case "subtract":
        result = in.A - in.B
    case "multiply":
        result = in.A * in.B
    case "divide":
        if in.B == 0 {
            return "", fmt.Errorf("division by zero")
        }
        result = in.A / in.B
    default:
        return "", fmt.Errorf("unknown action: %s", in.Action)
    }

    return fmt.Sprintf("%g %s %g = %g", in.A, in.Action, in.B, result), nil
}

func main() {
    app, err := nebo.New()
    if err != nil {
        log.Fatal(err)
    }
    app.RegisterTool(&Calculator{})
    log.Fatal(app.Run())
}

manifest.json:

json
{
    "id": "com.example.calculator",
    "name": "Calculator",
    "version": "1.0.0",
    "description": "Arithmetic calculator tool for the agent",
    "runtime": "local",
    "protocol": "grpc",
    "provides": ["tool:calculator"],
    "permissions": []
}

Build and install:

bash
go build -o binary .
mkdir -p ~/Library/Application\ Support/Nebo/apps/com.example.calculator
cp binary manifest.json ~/Library/Application\ Support/Nebo/apps/com.example.calculator/

Rust Example

rust
use async_trait::async_trait;
use nebo_sdk::{NeboApp, NeboError, SchemaBuilder};
use nebo_sdk::tool::ToolHandler;
use serde::Deserialize;
use serde_json::Value;

struct Calculator;

#[derive(Deserialize)]
struct Input { action: String, a: f64, b: f64 }

#[async_trait]
impl ToolHandler for Calculator {
    fn name(&self) -> &str { "calculator" }
    fn description(&self) -> &str { "Performs arithmetic calculations." }
    fn schema(&self) -> Value {
        SchemaBuilder::new(&["add", "subtract", "multiply", "divide"])
            .number("a", "First operand", true)
            .number("b", "Second operand", true)
            .build()
    }
    async fn execute(&self, input: Value) -> Result<String, NeboError> {
        let i: Input = serde_json::from_value(input)
            .map_err(|e| NeboError::Execution(e.to_string()))?;
        let r = match i.action.as_str() {
            "add" => i.a + i.b,
            "subtract" => i.a - i.b,
            "multiply" => i.a * i.b,
            "divide" => {
                if i.b == 0.0 {
                    return Err(NeboError::Execution("division by zero".into()));
                }
                i.a / i.b
            }
            other => return Err(NeboError::Execution(format!("unknown action: {other}"))),
        };
        Ok(format!("{} {} {} = {}", i.a, i.action, i.b, r))
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    NeboApp::new()?.register_tool(Calculator).run().await?;
    Ok(())
}

C Example

c
#include <nebo/nebo.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static int calculator_execute(const char *input_json, char **output, int *is_error) {
    /* Parse JSON, compute result, set *output */
    *output = strdup("2 add 3 = 5");
    *is_error = 0;
    return 0;
}

int main(void) {
    const char *actions[] = {"add", "subtract", "multiply", "divide", NULL};
    nebo_schema_builder_t *sb = nebo_schema_new(actions);
    nebo_schema_number(sb, "a", "First operand", 1);
    nebo_schema_number(sb, "b", "Second operand", 1);
    char *schema = nebo_schema_build(sb);
    nebo_schema_free(sb);

    nebo_tool_handler_t calculator = {
        .name = "calculator",
        .description = "Performs arithmetic calculations.",
        .schema = schema,
        .execute = calculator_execute,
    };

    nebo_app_t *app = nebo_app_new();
    nebo_app_register_tool(app, &calculator);
    int ret = nebo_app_run(app);
    free(schema);
    return ret;
}

Channel Apps

Declare "provides": ["channel:my_channel"] and "permissions": ["channel:send"].

Channel apps bridge external messaging platforms (Telegram, Discord, Slack, etc.) to Nebo's agent. When a user sends a message on the external platform, your app streams it to Nebo via Receive. When the agent wants to reply, Nebo calls your app's Send.

How it works

  1. Nebo calls ID() to get your channel's unique identifier
  2. Nebo calls Connect() with config from your app's settings
  3. Nebo opens a Receive() stream — your app sends inbound messages whenever a user messages the bot
  4. Inbound messages are routed to the agent's main conversation lane
  5. When the agent wants to send a message, Nebo calls Send() with the v1 message envelope
  6. On shutdown, Nebo calls Disconnect()

v1 Message Envelope

All channel messages — inbound and outbound — use the common message envelope:

json
{
  "message_id": "01953f8a-...",
  "channel_id": "telegram:12345",
  "sender": { "name": "Alex", "role": "COO", "bot_id": "uuid" },
  "text": "Q3 numbers look good...",
  "attachments": [{ "type": "image", "url": "https://...", "filename": "chart.png", "size": 45000 }],
  "reply_to": "01953f89-...",
  "actions": [{ "label": "Approve", "callback_id": "approve_q3" }],
  "platform_data": null,
  "timestamp": "2026-02-12T15:10:00Z"
}
FieldTypeDescription
message_idUUID v7Time-ordered unique ID
channel_id{type}:{platform_id}Route-able channel identifier
senderobjectBot identity — name, role, botId
textstringMessage body
attachmentsarrayFiles, images, audio
reply_toUUID v7Parent message ID for threading
actionsarrayInteractive buttons/keyboards
platform_databytesOpaque passthrough for platform-specific features
timestampISO 8601Publisher sets this

Two-layer design: The common fields (text, attachments, reply_to, actions) cover 90% of agent-initiated messaging. Platform-specific features go in platform_data — your plugin maps them to/from the native platform format.

Conversation Streams

Channel messages are routed over per-channel conversation streams for isolation:

text
neboai/bot/{botID}/channels/{channelType}/inbound    # messages from users → agent
neboai/bot/{botID}/channels/{channelType}/outbound   # messages from agent → channel

Go Example — Telegram Channel

go
package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "time"

    "github.com/google/uuid"
    tgbot "github.com/go-telegram/bot"
    nebo "github.com/NeboAI/nebo-sdk-go"
)

type Telegram struct {
    bot      *tgbot.Bot
    messages chan nebo.ChannelEnvelope
    cancel   context.CancelFunc
}

func (t *Telegram) ID() string { return "telegram" }

func (t *Telegram) Connect(_ context.Context, config map[string]string) error {
    token := config["bot_token"]
    if token == "" {
        return fmt.Errorf("bot_token is required")
    }

    botCtx, cancel := context.WithCancel(context.Background())
    t.cancel = cancel

    bot, err := tgbot.New(token, tgbot.WithDefaultHandler(
        func(bCtx context.Context, b *tgbot.Bot, update *tgbot.Update) {
            if update.Message == nil {
                return
            }
            env := nebo.ChannelEnvelope{
                MessageID: uuid.Must(uuid.NewV7()).String(),
                ChannelID: fmt.Sprintf("telegram:%d", update.Message.Chat.ID),
                Text:      update.Message.Text,
                Timestamp: time.Unix(int64(update.Message.Date), 0).UTC(),
            }
            t.messages <- env
        },
    ))
    if err != nil {
        return err
    }

    t.bot = bot
    go bot.Start(botCtx)
    return nil
}

func (t *Telegram) Send(ctx context.Context, env nebo.ChannelEnvelope) (string, error) {
    if t.bot == nil {
        return "", fmt.Errorf("not connected")
    }
    chatID := env.ChannelID[len("telegram:"):]
    params := &tgbot.SendMessageParams{
        ChatID: chatID,
        Text:   env.Text,
    }
    msg, err := t.bot.SendMessage(ctx, params)
    if err != nil {
        return "", err
    }
    return fmt.Sprintf("%d", msg.MessageID), nil
}

func (t *Telegram) Receive(_ context.Context) (<-chan nebo.ChannelEnvelope, error) {
    return t.messages, nil
}

func (t *Telegram) Disconnect(_ context.Context) error {
    if t.cancel != nil {
        t.cancel()
    }
    return nil
}

func main() {
    app, err := nebo.New()
    if err != nil {
        log.Fatal(err)
    }
    app.RegisterChannel(&Telegram{
        messages: make(chan nebo.ChannelEnvelope, 100),
    })
    log.Fatal(app.Run())
}

manifest.json:

json
{
    "id": "com.example.telegram",
    "name": "Telegram",
    "version": "1.0.0",
    "description": "Telegram messaging channel for Nebo",
    "runtime": "local",
    "protocol": "grpc",
    "provides": ["channel:telegram"],
    "permissions": ["channel:send", "network:outbound"],
    "settings": [
        {
            "key": "bot_token",
            "title": "Bot Token",
            "type": "password",
            "required": true,
            "description": "Telegram bot token from @BotFather",
            "secret": true
        }
    ]
}

More Capabilities

Comm App

Declare "provides": ["comm"] and "permissions": ["comm:*"]. Comm apps enable inter-agent communication.

FieldDescription
fromSender agent ID
toRecipient agent ID
topicMessage topic/channel
type"message", "mention", "proposal", "command", "info", "task"
contentMessage body

Key behaviors:

  • Register announces this agent on the network with its capabilities
  • Subscribe/Unsubscribe manage topic subscriptions
  • Receive streams inbound messages (server-streaming, same as channels)
  • Only one comm app can be active at a time

Gateway App

A gateway app routes LLM requests to models. Declare "provides": ["gateway"] and "permissions": ["network:outbound", "user:token"].

GatewayRequest contains messages, tools, system prompt, max tokens, temperature, and a UserContext (JWT token if user:token permission is granted).

GatewayEvent types:

TypeContentDescription
"text"Text chunkStreaming text token
"tool_call"JSONModel wants to call a tool
"thinking"Text chunkExtended thinking/reasoning
"error"Error messageSomething went wrong
"done"EmptyStream is complete

UI App

A UI app provides a custom configuration interface that opens in Nebo's built-in browser window. Declare "provides": ["ui"]. Unlike the automatic settings form (which Nebo renders from your settings manifest), a UI app serves its own full HTML/CSS/JS frontend.

Your app ships a ui/ directory containing static web assets (HTML, CSS, JS — typically a SPA with index.html). Nebo serves these files directly and proxies API calls to your app via gRPC.

PathMechanismPurpose
GET /api/v1/apps/{id}/ui/*Static file serverServes files from your ui/ directory (SPA fallback to index.html)
ANY /api/v1/apps/{id}/api/*gRPC proxyProxies HTTP requests to your app's UIService.HandleRequest()

The SDK wraps a standard net/http.ServeMux so you write normal Go HTTP handlers — the SDK handles the gRPC bridging automatically.

Go Example — Dashboard UI:

go
package main

import (
    "encoding/json"
    "log"
    "net/http"

    nebo "github.com/NeboAI/nebo-sdk-go"
)

type Dashboard struct {
    mux *http.ServeMux
}

func NewDashboard() *Dashboard {
    d := &Dashboard{mux: http.NewServeMux()}

    d.mux.HandleFunc("GET /status", func(w http.ResponseWriter, r *http.Request) {
        json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
    })

    d.mux.HandleFunc("POST /action", func(w http.ResponseWriter, r *http.Request) {
        var req map[string]string
        json.NewDecoder(r.Body).Decode(&req)
        json.NewEncoder(w).Encode(map[string]string{"result": "done"})
    })

    return d
}

func (d *Dashboard) Handler() *http.ServeMux { return d.mux }

func main() {
    app, err := nebo.New()
    if err != nil {
        log.Fatal(err)
    }
    app.RegisterUI(NewDashboard())
    log.Fatal(app.Run())
}

Your ui/index.html would make fetch calls to /api/v1/apps/com.example.dashboard/api/status, which get proxied through to your mux handler.

Schedule App

A schedule app replaces Nebo's built-in cron scheduler. Declare "provides": ["schedule"] and "permissions": ["schedule:create"].

When your schedule app launches, Nebo routes all scheduling through your app instead of the default cron engine. If your app crashes, Nebo automatically falls back to the built-in engine.

  • Your app owns the schedule state — Nebo doesn't store schedules in its own DB when a schedule app is active
  • Standard CRUD: Create, Get, List, Update, Delete, Enable, Disable
  • Triggers is a server-streaming RPC — your app sends a trigger message whenever a schedule fires
  • Only one schedule app can be active at a time

Multi-Capability Apps

An app can provide multiple capabilities. For example, a dashboard app with both a tool and a UI:

json
{
    "id": "com.example.dashboard",
    "name": "Dashboard",
    "version": "1.0.0",
    "provides": ["tool:dashboard_query", "ui"],
    "permissions": ["network:outbound"]
}
go
app, _ := nebo.New()
app.RegisterTool(&dashboardTool{})
app.RegisterUI(&dashboardUI{})
log.Fatal(app.Run())

Packaging

.napp Format

For distribution through NeboAI, package your app as a .napp file (a tar.gz archive):

bash
cd com.example.myapp/
tar -czf myapp-1.0.0.napp manifest.json binary signatures.json TOOL.md

Required files:

FileMax SizeDescription
manifest.json1 MBApp metadata
binary or app500 MBExecutable
signatures.json1 MBEd25519 signatures
TOOL.md1 MBTool instructions for the agent

Security rules during extraction:

  • No path traversal (../ rejected)
  • No symlinks (rejected)
  • No absolute paths (rejected)
  • Only allowlisted filenames accepted

Signatures

NeboAI signs apps with Ed25519. The signatures.json format:

json
{
    "key_id": "a1b2c3d4",
    "algorithm": "ed25519",
    "manifest_signature": "<base64 signature of raw manifest.json bytes>",
    "binary_sha256": "<hex SHA256 of binary>",
    "binary_signature": "<base64 signature of raw binary bytes>"
}

In dev mode (no NeboAI URL configured), signature verification is skipped entirely. You don't need signatures.json for local development.

Publishing

Once your app works locally, publish it through NeboAI for distribution to all Nebo users.

Developer Account Setup

  1. Register a NeboAI account at POST /api/v1/owners/register
  2. Log in to get a JWT token: POST /api/v1/owners/login
  3. Create a developer account: POST /api/v1/developer/accounts

Submission Flow

text
draft → pending_review → [approved] → published
                       → [rejected] → draft (fix and resubmit)

Step 1: Create the app

bash
curl -X POST https://neboai.com/api/v1/developer/apps \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My App",
    "version": "1.0.0",
    "description": "What it does",
    "category": "productivity",
    "visibility": "public"
  }'

Step 2: Upload platform binaries

bash
curl -X POST https://neboai.com/api/v1/developer/apps/{id}/binaries \
  -H "Authorization: Bearer $TOKEN" \
  -F "binary=@myapp-darwin-arm64" \
  -F "platform=darwin-arm64"

Upload one binary per platform: darwin-arm64, darwin-amd64, linux-amd64, linux-arm64.

Step 3: Submit for review

bash
curl -X POST https://neboai.com/api/v1/developer/apps/{id}/submit \
  -H "Authorization: Bearer $TOKEN"

Review Process

  1. Automated scan — binaries are scanned via VirusTotal
  2. Admin review — NeboAI team reviews metadata, manifest, and scan results
  3. On approval — NeboAI signs the app with Ed25519 and publishes it
  4. On rejection — you receive a reason and can fix and resubmit

Target review time: <24 hours.

Pushing Updates

  1. Update the app's version: PUT /api/v1/developer/apps/{id} with the new version
  2. Upload new binaries for each platform
  3. Submit for review again

Each update goes through the same review process. Previous versions remain available until the new version is approved.

Store Metadata

FieldDescription
categorye.g., productivity, communication, developer-tools
iconApp icon URL
visibilitypublic or private
descriptionCan be longer/richer than the manifest description

Dev Workflow

Local Development

No packaging required for local development:

  1. Create your app directory:
    mkdir -p ~/Library/Application\ Support/Nebo/apps/com.example.myapp
  2. Write your manifest.json and code
  3. Build your binary:
    go build -o ~/Library/Application\ Support/Nebo/apps/com.example.myapp/binary .
  4. Nebo auto-detects the new directory (or restart Nebo)
  5. After code changes, rebuild the binary — the file watcher will restart the app

Viewing Logs

App stdout/stderr are captured in per-app log files:

bash
tail -f ~/Library/Application\ Support/Nebo/apps/com.example.myapp/logs/stdout.log
tail -f ~/Library/Application\ Support/Nebo/apps/com.example.myapp/logs/stderr.log

Persistent Data

Use the data/ directory (also available as NEBO_APP_DATA env var) for any files your app needs to persist. This directory survives app updates and quarantine.

Debugging Tips

  • If your app doesn't start, check logs/stderr.log for errors
  • Make sure the binary is executable (chmod +x binary)
  • Make sure you're listening on the Unix socket path from NEBO_APP_SOCK
  • The socket must be ready within 10 seconds of launch
  • Use NEBO_APP_DATA for storage, not hardcoded paths
  • Print to stderr for debug logging (captured in logs/stderr.log)

Verifying

bash
# List installed apps
nebo apps list

# Check running apps via the web UI Settings > Apps
# Or check the apps directory for running sockets
ls ~/Library/Application\ Support/Nebo/apps/*/app.sock

Uninstalling

bash
nebo apps uninstall com.example.myapp

Skills

What Skills Are

Skills are instruction sets that shape how Nebo behaves during a conversation. When a skill is active, its content is injected directly into the system prompt — guiding the agent's tone, methodology, domain expertise, and tool usage for that session.

Skills are not code. They're markdown documents with YAML metadata. No compilation, no binary, no gRPC. A skill is a SKILL.md file in a directory. That's it.

Skills are contextual and temporary. They activate when relevant (via trigger matching or manual load), persist for a few turns, and expire when no longer needed. This keeps the agent's context lean — only the guidance that matters right now is in the prompt.

Skills complement apps. Apps provide executable capabilities (tools, channels, gateways). Skills provide orchestration guidance — how to use those tools effectively. A calendar app gives Nebo the ability to read and write events. A "meeting prep" skill tells Nebo how to prepare for meetings using that calendar app, email, and web search together.

Skills vs Apps

SkillsApps
FormatMarkdown (SKILL.md)Compiled binary (.napp)
RuntimeInjected into system promptSandboxed process over gRPC
ProvidesBehavioral guidance, methodologyTools, channels, UI panels
LifetimeSession-scoped, TTL-based expiryAlways running
CreationWrite a markdown fileWrite, compile, sign, package
SecurityNo permissions neededDeny-by-default sandbox

Quick Start

Create a directory in your Nebo skills folder with a SKILL.md file:

text
~/Library/Application Support/Nebo/skills/   # macOS
~/.config/nebo/skills/                        # Linux
%AppData%\Nebo\skills\                        # Windows
bash
mkdir -p ~/.config/nebo/skills/my-skill

Write a SKILL.md:

markdown
---
name: my-skill
description: One-line description of what this skill does
version: "1.0.0"
triggers:
  - keyword that activates this skill
  - another trigger phrase
---

# My Skill

Instructions for Nebo when this skill is active.

Tell the agent what to do, how to approach problems,
what tools to use, and what tone to take.

Nebo watches the skills directory with fsnotify. Your skill is available immediately — no restart needed. Say something that matches a trigger, and it activates automatically.

Or ask Nebo directly: "Create a skill for writing blog posts" — Nebo will write the SKILL.md file for you.

SKILL.md Format

Every skill is a single SKILL.md file with two parts: YAML frontmatter between --- markers, and a markdown body (the actual instructions injected into the system prompt).

FieldRequiredDefaultDescription
nameYesUnique identifier (becomes the slug)
descriptionYesOne-line description shown in the skill catalog
versionNo"1.0.0"Semver for tracking updates
authorNoSkill author
priorityNo0Higher = matched first (100+ for critical overrides)
max_turnsNo4Turns of inactivity before auto-expiry
triggersNo[]Phrases that auto-activate this skill (case-insensitive substring match)
toolsNo[]Tools this skill expects to use (informational)
dependenciesNo[]Other skills that must be installed
tagsNo[]For categorization and discovery
platformNo[]Supported platforms: macos, linux, windows (empty = all)

How Skills Activate

Skills follow a two-phase activation model: hint then invoke.

Phase 1: Trigger Matching — Every time a user sends a message, Nebo scans all registered skills for trigger matches. Matching skills appear as hints in the system prompt. The agent sees the hint and decides whether to invoke the skill.

Phase 2: Invocation — The agent invokes a skill by calling:

text
skill(name: "code-review")

This returns the full SKILL.md template as the tool result, records the invocation, and re-injects the template into the system prompt on subsequent turns.

TTL and Expiry:

  • Auto-matched skills: 4 turns of inactivity (configurable via max_turns)
  • Manually loaded skills: 6 turns of inactivity
  • Re-invocation resets the timer
  • Hard cap: maximum 4 active skills per session
  • Token budget: combined active skill content capped at 16,000 characters

Managing Skills

The agent can manage skills through the skill tool:

ActionDescriptionExample
catalogList all available skillsskill(action: "catalog")
loadActivate for this sessionskill(name: "code-review", action: "load")
unloadDeactivate a skillskill(name: "code-review", action: "unload")
createCreate a new skill on diskskill(action: "create", content: "...")
updateUpdate an existing skillskill(name: "my-skill", action: "update", content: "...")
deleteDelete a skill from diskskill(name: "my-skill", action: "delete")

Writing Good Skills

A good skill template has:

  1. A clear heading — tells the agent what mode it's in
  2. Core principles — the non-negotiable behavioral rules
  3. Methodology — step-by-step approach for common scenarios
  4. Examples — concrete user-message-to-response patterns
  5. Anti-patterns — what NOT to do (models learn well from negative examples)

Tip: Each skill should do one thing well. Don't create a "general programming" skill — create code-review, debugging, api-design as separate skills. The agent can have multiple active simultaneously.

Bundled Skills

Nebo ships with bundled skills covering common use cases:

SkillPriorityDescription
onboarding100New user greeting and profile collection
best-friend50Personality skill — casual, loyal, real talk
debugging25Systematic debugging methodology
api-design15RESTful API design best practices
code-review10Structured code review with severity levels
database-expert10Database design, queries, optimization
git-workflow10Git branching, commits, PR workflows
security-audit10Security analysis and vulnerability detection

These serve as both useful defaults and reference implementations for creating your own skills.

Reference

Proto Files

Proto files live in proto/apps/v0/. The SDKs ship with pre-generated code, so you don't need protoc for normal development.

FileServiceKey RPCs
v0/common.proto(messages only)HealthCheckRequest/Response, SettingsMap, UserContext, Empty
v0/tool.protoToolServiceName, Description, Schema, Execute, RequiresApproval, Configure
v0/channel.protoChannelServiceID, Connect, Disconnect, Send, Receive (stream), Configure
v0/comm.protoCommServiceName, Version, Connect, Disconnect, Send, Subscribe, Register, Receive (stream), Configure
v0/gateway.protoGatewayServiceHealthCheck, Stream (stream), Poll, Cancel, Configure
v0/ui.protoUIServiceHealthCheck, HandleRequest, Configure
v0/schedule.protoScheduleServiceHealthCheck, Create, Get, List, Update, Delete, Enable, Disable, Trigger, History, Triggers (stream), Configure

Every service includes HealthCheck and Configure RPCs. The SDK handles both automatically.

Channel Proto Types

Message TypeDescription
ChannelSendRequestOutbound message envelope with message_id, channelId, sender, text, attachments, reply_to, actions, platform_data
ChannelSendResponsePlatform message ID for threading
InboundMessageInbound message envelope with all common fields + timestamp
MessageSenderBot identity (name, role, botId) — enriched by broker
AttachmentFile/image/audio attachment (type, url, filename, size)
MessageActionInteractive button/keyboard action (label, callback_id)

Bot Identity and Roles

Nebo bots have a three-axis identity model:

  • creature — What the bot is (archetype): "Quick-witted strategist", "Meticulous researcher"
  • role — How it relates to the user (relationship): "Friend", "COO", "Mentor"
  • vibe — Its energy (communication style): "Chill but opinionated", "Warm and encouraging"

When your channel plugin receives an outbound message, the sender field contains the bot's name and role. Use this to format the display name for your platform. The role isn't a job title — it's a relationship descriptor. "Friend", "Son", and "Mentor" are valid roles alongside "COO" and "DevLead".