9.1 KiB
Extension Authoring
nanobot extensions are packages with a strict nanobot.extension.json
manifest. The manifest can be inspected without importing optional SDKs or
executing package code. Runtime activation then projects each contribution into
the native nanobot registry that owns that capability.
This guide covers native Python extensions and the compatibility boundary for Pi and OpenClaw packages. Users installing packages should read Extensions.
Package Layout
A minimal native package is:
my-extension/
|-- nanobot.extension.json
`-- my_extension/
`-- __init__.py
nanobot.extension.json:
{
"id": "acme.review",
"name": "Acme Review",
"version": "1.0.0",
"apiVersion": 1,
"runtime": "python",
"entry": "my_extension:register",
"description": "Adds a review tool and command.",
"homepage": "https://example.com/acme-review",
"license": "MIT",
"contributions": [
{
"kind": "tool",
"name": "review_code",
"description": "Review a local change."
},
{
"kind": "command",
"name": "review",
"description": "Start a review from chat."
}
],
"dependencies": [
{
"kind": "python",
"name": "acme-review-core",
"specifier": ">=1,<2",
"optional": false
}
],
"permissions": [
{
"name": "workspace.read",
"reason": "Read files selected for review."
}
]
}
Unknown fields are rejected. This is deliberate: a misspelled permission, dependency, or contribution must not silently change package behavior.
Manifest Reference
Top-level fields
| Field | Required | Meaning |
|---|---|---|
id |
yes | Stable lowercase package identity |
name |
yes | Human-readable name |
version |
yes | Installed package version |
apiVersion |
no | Manifest API, currently 1 |
runtime |
yes | python, pi, openclaw, or declarative |
entry |
runtime-dependent | One activation entry |
entries |
runtime-dependent | Multiple Pi/OpenClaw entries; takes precedence over entry |
contributions |
no | Capabilities owned by the package |
dependencies |
no | Activation prerequisites |
permissions |
no | Privileged host capabilities requested from the user |
description |
no | Catalog summary |
homepage |
no | Project or documentation URL |
license |
no | SPDX-style license label |
Entries must be relative to the package root and cannot contain ...
Contributions
Supported kind values are:
tool
skill
channel
llm_provider
transcription_provider
image_generation_provider
web_search_provider
mcp_server
hook
command
webui
Each contribution has a stable name, optional runtime target, optional
description, and optional replaces list. replaces contains extension IDs,
not contribution names. Replacement is still checked against scope precedence.
The manifest declares ownership. It does not create an implementation by itself. A runtime must register the corresponding native capability.
Dependencies
| Kind | name identifies |
Version behavior |
|---|---|---|
python |
Installed Python distribution | PEP 440 specifier |
npm |
Package under the extension's node_modules |
npm installation constraint |
executable |
Command on PATH |
No version probe |
environment |
Environment variable | Must be non-empty |
extension |
Another extension ID | Installed extension version |
Set optional: true when the extension can activate without the dependency.
Do not put API keys in the manifest.
Permissions
Permission names are lowercase namespaced identifiers such as
workspace.read, workspace.write, or network.http. Include a concrete
reason the user can evaluate. Activation requires every requested permission to
be granted.
Permissions describe host policy; they are not an OS sandbox. Keep the request set minimal and use native host operations when one exists.
Native Python Runtime
The Python entry is module[:attribute]; the default attribute is register.
The function is synchronous and must return None:
from nanobot.agent.tools.base import Tool
class ReviewTool(Tool):
@property
def name(self) -> str:
return "review_code"
@property
def description(self) -> str:
return "Review a local code change."
@property
def parameters(self) -> dict:
return {
"type": "object",
"properties": {
"path": {"type": "string"},
},
"required": ["path"],
}
async def execute(self, path: str) -> str:
return f"Review requested for {path}"
def register(api) -> None:
api.register_tool(ReviewTool())
The v1 Python API exposes:
register_tool(tool)register_command(command, handler, prefix=False)register_hook_factory(factory)
These methods use existing nanobot registries. Do not import or patch
AgentLoop, reach into WebSocket internals, or create a second tool registry.
If a new contribution kind needs execution support, add an adapter at the
native subsystem boundary and keep the manifest API independent from that
implementation.
Publish and Discover
For npm discovery, include the exact nanobot-extension keyword:
{
"keywords": [
"nanobot-extension"
]
}
Compatibility packages use their upstream metadata and keyword:
- Pi:
pi-packagepluspi.extensions - OpenClaw:
openclaw-pluginplusopenclaw.extensionsoropenclaw.runtimeExtensions
Native Python packages can be installed from a local directory or Git source. The market is an index; installation always passes through local validation, dependency checks, permission review, trust, and activation.
Compatibility Matrix
The matrix describes the current adapter, not an intention to support every upstream API.
| Upstream capability | Pi | OpenClaw | nanobot behavior |
|---|---|---|---|
| Tool registration and calls | Executable | Executable | Projected as native Tool; implementation stays in Node sidecar |
| Slash commands | Executable | Executable | Registered in native command router |
| Supported lifecycle events | Observation-only | Observation-only | Receives serialized run/tool events; cannot mutate native context |
| Provider registration | Metadata only | Metadata only | Visible in catalog and diagnostics; not an executable provider |
| Transcription/image/web-search provider contracts | N/A | Metadata only | Visible but not projected into native provider registries |
| Skills declared in plugin metadata | N/A | Metadata only | Catalog ownership only; runtime skill installation is not synthesized |
| Channels | N/A | Metadata only | No OpenClaw channel host emulation |
| Session tree and custom entries | Unsupported | Unsupported | No compatible host surface |
| Terminal UI, widgets, renderers, shortcuts | Unsupported | Unsupported | WebUI and CLI have different rendering contracts |
| Model selection and thinking control | Unsupported | Unsupported | Remains owned by nanobot model presets and request policy |
| Arbitrary upstream host services | Unsupported | Unsupported | Reported as diagnostics rather than silently emulated |
Pi package shape
{
"name": "@acme/pi-review",
"version": "1.0.0",
"keywords": ["pi-package"],
"pi": {
"extensions": ["./index.ts"]
}
}
The sidecar supports registerTool, registerCommand, and selected on(...)
lifecycle handlers. TypeScript uses Node's native type stripping when
available, with jiti as a fallback installed with the package runtime.
OpenClaw package shape
{
"name": "@acme/openclaw-review",
"version": "1.0.0",
"keywords": ["openclaw-plugin"],
"openclaw": {
"runtimeExtensions": ["./dist/index.js"]
}
}
If present, openclaw.plugin.json supplies catalog identity, contribution
contracts, command aliases, and compatibility diagnostics. The OpenClaw
register function must complete synchronously during load.
Test an Extension
Use an isolated config and workspace while developing:
nanobot extensions install ./my-extension --kind local
nanobot extensions inspect acme.review
nanobot extensions permissions acme.review workspace.read
nanobot extensions trust acme.review
nanobot extensions enable acme.review
nanobot agent -m "Use review_code on README.md"
Also test:
- install while untrusted does not execute code;
- missing hard dependencies leave the package inactive;
- denied or missing permissions prevent activation;
- duplicate contribution names become diagnostics;
- disable, untrust, reload, and uninstall remove runtime registrations;
- one broken package does not block unrelated packages.
For nanobot itself, extension tests live under tests/extensions/. Keep
compatibility fixtures small and assert diagnostics for unsupported APIs.
Design Rules
- Extend the control plane, not the core loop.
- Keep one native owner for each capability.
- Make discovery metadata-only.
- Separate install, permission grant, trust, and enable.
- Report partial compatibility honestly.
- Keep market metadata independent from runtime execution.
- Roll back all registrations owned by a failed or unloaded extension.