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.
| Status | Languages |
|---|---|
| Supported | Go (recommended), Rust, C/C++, Zig |
| Rejected | Python, 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):
go get github.com/NeboAI/nebo-sdk-goRust (coming soon):
# Coming soon
[dependencies]
nebo-sdk = "0.1"C/C++ (coming soon):
# 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:
~/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
- Create
manifest.jsonwithid,name,version,provides - Write
TOOL.mddescribing how the agent should use your tool - Install the SDK (
go get github.com/NeboAI/nebo-sdk-go) - Implement the handler interface for your capability (
ToolHandler,ChannelHandler, etc.) - Register the handler and call
app.Run() - Build a native binary (
go build -o binary .) - Place binary + manifest.json + TOOL.md in
apps/<your-app-id>/directory - Nebo auto-discovers and launches it
Configuration
manifest.json
Every app requires a manifest.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:
| Field | Description |
|---|---|
id | Reverse-domain identifier (e.g., com.mycompany.myapp) |
name | Human-readable name |
version | Semver string |
provides | At least one capability (see below) |
Optional fields:
| Field | Default | Description |
|---|---|---|
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:
| Capability | gRPC Service | Description |
|---|---|---|
gateway | GatewayService | LLM model routing |
tool:<name> | ToolService | A named tool for the agent |
channel:<name> | ChannelService | A messaging channel |
comm | CommService | Inter-agent communication |
ui | UIService | Custom UI with HTTP proxy |
schedule | ScheduleService | Custom scheduling |
vision | ToolService | Vision processing |
browser | ToolService | Browser 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.
| Prefix | Examples | Description |
|---|---|---|
network: | network:outbound, network:* | Network access |
filesystem: | filesystem:read, filesystem:write | File system access |
memory: | memory:read, memory:write | Agent memory access |
session: | session:read | Conversation sessions |
tool: | tool:shell, tool:file | Use other tools |
shell: | shell:exec | Shell command execution |
channel: | channel:send, channel:* | Channel operations |
comm: | comm:send, comm:* | Inter-agent comm |
model: | model:chat | AI model access |
user: | user:token | Receive user JWT in requests |
schedule: | schedule:create | Cron job management |
database: | database:query | Database access |
storage: | storage:read, storage:write | Persistent storage |
context: | context:read | Agent context access |
subagent: | subagent:spawn | Sub-agent operations |
lane: | lane:enqueue | Lane operations |
notification: | notification:send | Push notifications |
embedding: | embedding:search | Vector embedding access |
skill: | skill:invoke | Skill invocation |
advisor: | advisor:consult | Advisor system access |
mcp: | mcp:connect | MCP server access |
voice: | voice:record | Voice/audio access |
browser: | browser:navigate | Browser automation |
oauth: | oauth:google, oauth:* | OAuth token access |
settings: | settings:read | Settings access |
capability: | capability:register | Capability 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.
| Type | Description |
|---|---|
text | Single-line text input |
password | Masked text input |
toggle | Boolean on/off switch |
select | Dropdown with predefined options |
number | Numeric input |
url | URL input with validation |
{
"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.
{
"oauth": [
{
"provider": "google",
"scopes": ["https://www.googleapis.com/auth/calendar"]
}
]
}| Field | Description |
|---|---|
provider | OAuth provider: "google", "microsoft", "github" |
scopes | Array 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:
| Variable | Value |
|---|---|
NEBO_APP_DIR | App's installation directory |
NEBO_APP_SOCK | Path to the Unix socket to create |
NEBO_APP_ID | App ID from manifest |
NEBO_APP_NAME | App name from manifest |
NEBO_APP_VERSION | App version from manifest |
NEBO_APP_DATA | Path to app's data/ directory |
PATH | System PATH (passthrough) |
HOME | User home directory (passthrough) |
TMPDIR | Temp 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
- Nebo reads
manifest.jsonand validates it - Finds
binaryorappexecutable in the app directory - Checks for
.quarantinedmarker (refuses to launch quarantined apps) - Revocation check (NeboAI-distributed apps only)
- Signature verification (NeboAI-distributed apps only, skipped in dev)
- Binary validation (rejects symlinks, scripts, non-executables, oversized files)
- Cleans up stale socket from previous run
- Creates
data/directory for sandboxed storage - Sets up per-app log files (
logs/stdout.log,logs/stderr.log) - Starts binary with sanitized environment and process group isolation
- Waits for Unix socket to appear (exponential backoff, max 10 seconds)
- Connects via gRPC over the Unix socket
- Creates capability-specific gRPC clients based on
provides - Runs health check
- 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):
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):
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:
- Always include an
actionfield as a required string enum - Add a
resourcefield (required string enum) only if your tool manages multiple distinct resource types - All other fields are action-specific parameters
- The
actionenum description should list all available actions
Why STRAP matters:
- LLMs learn the
actionrouting 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
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:
{
"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:
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
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
#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
- Nebo calls
ID()to get your channel's unique identifier - Nebo calls
Connect()with config from your app's settings - Nebo opens a
Receive()stream — your app sends inbound messages whenever a user messages the bot - Inbound messages are routed to the agent's main conversation lane
- When the agent wants to send a message, Nebo calls
Send()with the v1 message envelope - On shutdown, Nebo calls
Disconnect()
v1 Message Envelope
All channel messages — inbound and outbound — use the common message envelope:
{
"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"
}| Field | Type | Description |
|---|---|---|
message_id | UUID v7 | Time-ordered unique ID |
channel_id | {type}:{platform_id} | Route-able channel identifier |
sender | object | Bot identity — name, role, botId |
text | string | Message body |
attachments | array | Files, images, audio |
reply_to | UUID v7 | Parent message ID for threading |
actions | array | Interactive buttons/keyboards |
platform_data | bytes | Opaque passthrough for platform-specific features |
timestamp | ISO 8601 | Publisher 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:
neboai/bot/{botID}/channels/{channelType}/inbound # messages from users → agent
neboai/bot/{botID}/channels/{channelType}/outbound # messages from agent → channelGo Example — Telegram Channel
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:
{
"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.
| Field | Description |
|---|---|
from | Sender agent ID |
to | Recipient agent ID |
topic | Message topic/channel |
type | "message", "mention", "proposal", "command", "info", "task" |
content | Message body |
Key behaviors:
Registerannounces this agent on the network with its capabilitiesSubscribe/Unsubscribemanage topic subscriptionsReceivestreams 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:
| Type | Content | Description |
|---|---|---|
"text" | Text chunk | Streaming text token |
"tool_call" | JSON | Model wants to call a tool |
"thinking" | Text chunk | Extended thinking/reasoning |
"error" | Error message | Something went wrong |
"done" | Empty | Stream 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.
| Path | Mechanism | Purpose |
|---|---|---|
GET /api/v1/apps/{id}/ui/* | Static file server | Serves files from your ui/ directory (SPA fallback to index.html) |
ANY /api/v1/apps/{id}/api/* | gRPC proxy | Proxies 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:
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 Triggersis 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:
{
"id": "com.example.dashboard",
"name": "Dashboard",
"version": "1.0.0",
"provides": ["tool:dashboard_query", "ui"],
"permissions": ["network:outbound"]
}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):
cd com.example.myapp/
tar -czf myapp-1.0.0.napp manifest.json binary signatures.json TOOL.mdRequired files:
| File | Max Size | Description |
|---|---|---|
manifest.json | 1 MB | App metadata |
binary or app | 500 MB | Executable |
signatures.json | 1 MB | Ed25519 signatures |
TOOL.md | 1 MB | Tool 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:
{
"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
- Register a NeboAI account at
POST /api/v1/owners/register - Log in to get a JWT token:
POST /api/v1/owners/login - Create a developer account:
POST /api/v1/developer/accounts
Submission Flow
draft → pending_review → [approved] → published
→ [rejected] → draft (fix and resubmit)Step 1: Create the app
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
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
curl -X POST https://neboai.com/api/v1/developer/apps/{id}/submit \
-H "Authorization: Bearer $TOKEN"Review Process
- Automated scan — binaries are scanned via VirusTotal
- Admin review — NeboAI team reviews metadata, manifest, and scan results
- On approval — NeboAI signs the app with Ed25519 and publishes it
- On rejection — you receive a reason and can fix and resubmit
Target review time: <24 hours.
Pushing Updates
- Update the app's version:
PUT /api/v1/developer/apps/{id}with the new version - Upload new binaries for each platform
- Submit for review again
Each update goes through the same review process. Previous versions remain available until the new version is approved.
Store Metadata
| Field | Description |
|---|---|
category | e.g., productivity, communication, developer-tools |
icon | App icon URL |
visibility | public or private |
description | Can be longer/richer than the manifest description |
Dev Workflow
Local Development
No packaging required for local development:
- Create your app directory:
mkdir -p ~/Library/Application\ Support/Nebo/apps/com.example.myapp - Write your
manifest.jsonand code - Build your binary:
go build -o ~/Library/Application\ Support/Nebo/apps/com.example.myapp/binary . - Nebo auto-detects the new directory (or restart Nebo)
- 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:
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.logPersistent 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.logfor 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_DATAfor storage, not hardcoded paths - Print to stderr for debug logging (captured in
logs/stderr.log)
Verifying
# 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.sockUninstalling
nebo apps uninstall com.example.myappSkills
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
| Skills | Apps | |
|---|---|---|
| Format | Markdown (SKILL.md) | Compiled binary (.napp) |
| Runtime | Injected into system prompt | Sandboxed process over gRPC |
| Provides | Behavioral guidance, methodology | Tools, channels, UI panels |
| Lifetime | Session-scoped, TTL-based expiry | Always running |
| Creation | Write a markdown file | Write, compile, sign, package |
| Security | No permissions needed | Deny-by-default sandbox |
Quick Start
Create a directory in your Nebo skills folder with a SKILL.md file:
~/Library/Application Support/Nebo/skills/ # macOS
~/.config/nebo/skills/ # Linux
%AppData%\Nebo\skills\ # Windowsmkdir -p ~/.config/nebo/skills/my-skillWrite a SKILL.md:
---
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).
| Field | Required | Default | Description |
|---|---|---|---|
name | Yes | — | Unique identifier (becomes the slug) |
description | Yes | — | One-line description shown in the skill catalog |
version | No | "1.0.0" | Semver for tracking updates |
author | No | — | Skill author |
priority | No | 0 | Higher = matched first (100+ for critical overrides) |
max_turns | No | 4 | Turns of inactivity before auto-expiry |
triggers | No | [] | Phrases that auto-activate this skill (case-insensitive substring match) |
tools | No | [] | Tools this skill expects to use (informational) |
dependencies | No | [] | Other skills that must be installed |
tags | No | [] | For categorization and discovery |
platform | No | [] | 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:
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:
| Action | Description | Example |
|---|---|---|
catalog | List all available skills | skill(action: "catalog") |
load | Activate for this session | skill(name: "code-review", action: "load") |
unload | Deactivate a skill | skill(name: "code-review", action: "unload") |
create | Create a new skill on disk | skill(action: "create", content: "...") |
update | Update an existing skill | skill(name: "my-skill", action: "update", content: "...") |
delete | Delete a skill from disk | skill(name: "my-skill", action: "delete") |
Writing Good Skills
A good skill template has:
- A clear heading — tells the agent what mode it's in
- Core principles — the non-negotiable behavioral rules
- Methodology — step-by-step approach for common scenarios
- Examples — concrete user-message-to-response patterns
- 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:
| Skill | Priority | Description |
|---|---|---|
onboarding | 100 | New user greeting and profile collection |
best-friend | 50 | Personality skill — casual, loyal, real talk |
debugging | 25 | Systematic debugging methodology |
api-design | 15 | RESTful API design best practices |
code-review | 10 | Structured code review with severity levels |
database-expert | 10 | Database design, queries, optimization |
git-workflow | 10 | Git branching, commits, PR workflows |
security-audit | 10 | Security 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.
| File | Service | Key RPCs |
|---|---|---|
v0/common.proto | (messages only) | HealthCheckRequest/Response, SettingsMap, UserContext, Empty |
v0/tool.proto | ToolService | Name, Description, Schema, Execute, RequiresApproval, Configure |
v0/channel.proto | ChannelService | ID, Connect, Disconnect, Send, Receive (stream), Configure |
v0/comm.proto | CommService | Name, Version, Connect, Disconnect, Send, Subscribe, Register, Receive (stream), Configure |
v0/gateway.proto | GatewayService | HealthCheck, Stream (stream), Poll, Cancel, Configure |
v0/ui.proto | UIService | HealthCheck, HandleRequest, Configure |
v0/schedule.proto | ScheduleService | HealthCheck, 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 Type | Description |
|---|---|
ChannelSendRequest | Outbound message envelope with message_id, channelId, sender, text, attachments, reply_to, actions, platform_data |
ChannelSendResponse | Platform message ID for threading |
InboundMessage | Inbound message envelope with all common fields + timestamp |
MessageSender | Bot identity (name, role, botId) — enriched by broker |
Attachment | File/image/audio attachment (type, url, filename, size) |
MessageAction | Interactive 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".