How to Develop Extensions¶
Overview¶
Extensions are the primary way to extend Zelos capabilities beyond its core functionality. They enable developers to create reusable, distributable packages that integrate seamlessly with the Zelos ecosystem. Each extension runs in an isolated environment and communicates with the Zelos Agent through well-defined interfaces.
Quick Start
Jump to Creating Your First Extension if you want to dive right in, or continue reading for a comprehensive understanding of the extension system.
Why Build Extensions?¶
Extensions allow you to:
- Share protocols and integrations with the global Zelos community
- Accelerate development by building on the robust Zelos SDK
- Package domain expertise into reusable tools that others can install instantly
What Can Extensions Do?¶
Extensions can:
- Stream data from any source (hardware, APIs, files, networks)
- Decode protocols (CAN, Modbus, MQTT, custom binary formats)
- Control hardware through interactive actions and automation
- Process and transform data in real-time
- Integrate with external services and cloud platforms
- Provide custom visualizations through the Zelos App
Core Concepts¶
Extension Identity¶
Zelos automatically derives extension IDs - you don't specify an internal id field:
Marketplace extensions: owner.repo format
- Derived from your GitHub repository (e.g., acme/canbus-decoder → acme.canbus-decoder)
- Automatically lowercased and normalized
Local extensions: local.<foldername> format
- Derived from your project directory name
- Example: ~/my-sensor-monitor/ → local.my-sensor-monitor
ID Validation Rules¶
Extension IDs must follow these rules:
- Format:
publisher.name(dot-separated segments) - Character rules:
- Lowercase letters, digits, and hyphens only
- No uppercase letters
- No leading or trailing hyphens in segments
- No empty segments
- Examples:
- ✅
acme.canbus-decoder - ✅
john-doe.mqtt-bridge - ✅
local.sensor-01 - ❌
Acme.CANBus(uppercase) - ❌
acme.-canbus(leading hyphen) - ❌
acme..canbus(empty segment)
Local Extension Normalization¶
For local installations, Zelos normalizes your folder name:
- Convert to lowercase
- Replace non-alphanumeric characters with hyphens
- Collapse multiple consecutive hyphens (
--→-) - Trim leading/trailing hyphens
- Prefix with
local.
Examples:
Sensor_Monitor/ → local.sensor-monitor
My Extension v2/ → local.my-extension-v2
CAN__BUS___Tool/ → local.can-bus-tool
Runtime Environment¶
Extensions run as Python applications using Python 3.10 through 3.14 (major.minor format). UV automatically manages Python downloads and virtual environments.
Security Model¶
Extensions operate with:
- Process isolation - Each runs in its own process
- File system boundaries - Confined to their installation directory
- Minimal environment - Limited environment variables for security
- Graceful lifecycle - Proper startup and shutdown handling
Installation Layout¶
When an extension installs, the agent creates a dedicated container directory at
<data>/extensions/<id>/<version>/:
- Marketplace installs unpack into a
code/subdirectory. The agent stores runtime files (.venv,.cache,.last_config, logs, etc.) alongside the code but outside the tree you shipped, then markscode/as read-only and records an integrity hash. Any manual edits invalidate that hash and the supervisor refuses to start the extension. - Local installs still hot-reload by symlinking
code/to your working copy, but the runtime artifacts now live in the container directory instead of polluting your repo.
Windows Developer Mode required
Local installs rely on symlinks. Enable Windows Developer Mode (or otherwise allow symlink creation) before installing a local extension on Windows.
Creating Your First Extension¶
Use the official Cookiecutter template to generate a production-ready extension:
You'll be prompted for:
- Author – Your name (e.g., "Jane Doe")
- Email – Contact email
- GitHub Handle – Your GitHub username
- Project Name – Extension name in kebab-case (e.g., "sensor-monitor")
- Description – Short summary (auto-generated if skipped)
The template automatically generates a complete project with:
- ✅ Complete extension structure with working example
- ✅ Development tools:
uv,just,ruff,pytest - ✅ Pre-commit hooks for code quality
- ✅ GitHub Actions CI/CD workflows
- ✅ Professional documentation (README, CONTRIBUTING, CHANGELOG)
- ✅ Tests and proper project organization
- ✅ VSCode integration with recommended extensions and settings
- ✅ Automated dependency updates via Dependabot
Generated Project Structure¶
my-zelos-extension/
├── extension.toml # Extension manifest
├── main.py # Entry point
├── config.schema.json # Configuration UI schema
├── pyproject.toml # Dependencies and metadata
├── my_zelos_extension/ # Application source
│ ├── extension.py # Core: SensorMonitor class with actions
│ └── utils/
│ └── __init__.py # Your utility functions (if needed)
├── tests/
│ └── test_extension.py # Extension tests
├── assets/
│ └── icon.svg # Extension icon
├── scripts/
│ └── package_extension.py # Packaging script
├── .vscode/
│ ├── settings.json # VSCode settings (formatting, testing)
│ └── extensions.json # Recommended extensions
├── .github/
│ ├── workflows/
│ │ ├── CI.yml # CI on pushes/PRs
│ │ └── release.yml # Release automation on tags
│ └── dependabot.yml # Automated dependency updates
├── Justfile # Commands: install/dev/check/test/package/release
├── README.md # User documentation
└── CONTRIBUTING.md # Developer documentation
Development Workflow¶
# Enter your project
cd my-zelos-extension
# Install dependencies and set up pre-commit hooks
just install
# Run all checks
just check # Linting with ruff
just test # Run test suite
# Run extension locally
just dev
# Create a release
just release 0.1.0 # Update version, run checks, commit, and create tag
git push origin main v0.1.0
The template includes a working sensor monitor example that demonstrates:
- Schema definition with proper types and units
- Multiple event types - Example sensor monitoring
- Interactive actions for runtime control
- Synchronous data streaming with clean lifecycle management
- Configuration loading with fallback to defaults
- Graceful shutdown handling with signal handlers
- Code quality tools - Ruff for linting and formatting
- VSCode integration - Instant setup with recommended extensions and settings
- Automated updates - Dependabot keeps dependencies current
Publishing to Marketplace¶
Initial Setup:
- Create a repository on GitHub (e.g.,
your-org/my-extension) - Push your code:
Publishing a Release:
- Create and tag a release:
just release 0.1.0 && git push origin main v0.1.0 - GitHub Actions automatically packages and creates a release
- Submit your repository URL to the Zelos Marketplace
- After approval, users can install directly from the Zelos App
Extension Package Structure¶
Archive Validation and Enforcement¶
The marketplace and extension manager enforce strict validation to ensure security and reliability:
Size Limits¶
| Limit | Value | Enforced |
|---|---|---|
| Compressed size | 500 MiB | Marketplace ingestion + Download |
| Extracted size | 2 GiB | Extraction time |
| File count | 10,000 files | Extraction time |
Security Validation¶
During extraction, Zelos rejects archives containing:
- Symbolic links or hard links (prevents link-based escapes)
- Special files (block devices, character devices, FIFOs)
- Path traversal attempts (
../, absolute paths) - Hidden directories or files (starting with
.) - Parent directory references anywhere in paths
Integrity Verification¶
- SHA-256 checksum verified for every download
- Checksum provided by marketplace (computed during ingestion)
- Mismatch causes installation to fail immediately
When Validation Happens¶
- Marketplace ingestion: Size and format checks before publication
- Download: Size and checksum verification
- Extraction: Security validation (symlinks, path traversal, special files)
Validation Failures Prevent Installation
Archives that fail any validation check will not install. Ensure your package script creates clean archives without symlinks or hidden files.
Archive Requirements¶
Extensions must be packaged as compressed archives with specific requirements:
- Supported formats:
.tar.gz,.tgz,.zip - Size limit: 500 MiB compressed
- Extracted size limit: 2 GiB
- File count limit: 10,000 files
my-extension.tar.gz
├── extension.toml # REQUIRED: Manifest file
├── main.py # Entry point (Python)
├── pyproject.toml # Python dependencies (modern package management)
├── uv.lock # Locked dependencies (ensures reproducible installs)
├── README.md # Marketplace documentation (optional but recommended)
├── config.schema.json # Configuration schema (if applicable)
├── my_extension/ # Python package containing source code
│ └── __init__.py
└── ... # Any additional assets you reference (icons, changelog, etc.)
No Parent Directory
Files must be at the archive root. Do NOT wrap in an extra directory.
- ❌ No hidden files/directories (starting with
.) - ❌ No parent directory references (
..) - ❌ No symbolic or hard links
- ❌ No absolute paths
- ✅ All paths must be relative
- ✅ Forward slashes only (
/)
Hidden files are now rejected during extraction—make sure your packaging script removes
artifacts such as .git/, .DS_Store, or editor-specific folders before creating the archive.
The Extension Manifest¶
The extension.toml file is the heart of your extension - it defines identity, dependencies, runtime configuration, and metadata. Every field is validated to ensure security and compatibility.
Minimal Manifest¶
Required fields only:
name = "My Extension" # Display name
version = "1.0.0" # SemVer version
# Runtime
[runtime]
type = "python" # Python runtime (only supported type)
entry = "main.py" # Entry point file
Defaults
All other fields are optional with sensible defaults:
Manifest defaults (applied when field omitted):
python_version→"3.11"workdir→"."(top-level field)zelos.version→"^25.0.20"stop.grace_seconds→10config.schema→"config.schema.json"(when[config]section present)targets→[](empty = all platforms)env→{}(no variables)keywords→[],categories→[]
Complete Manifest Reference¶
# === REQUIRED FIELDS ===
name = "CANbus Decoder Pro"
version = "2.1.0"
description = "Decode CAN messages with DBC files and stream to Zelos"
author = "Acme Corporation"
repository = "https://github.com/acme/canbus-decoder"
[zelos]
version = "^25.0.20" # SemVer range for agent compatibility (default: "^25.0.20", can be omitted)
# === RUNTIME CONFIGURATION ===
workdir = "." # Working directory (relative to extension root)
[runtime]
type = "python" # Python runtime (only supported type)
entry = "main.py" # Entry point script
python_version = "3.11" # Supported: "3.10" through "3.14"
# Platform targets (empty = all platforms)
targets = [
"linux-x86_64", # Linux Intel/AMD
"linux-aarch64", # Linux ARM64
"darwin-x86_64", # macOS Intel
"darwin-arm64", # macOS Apple Silicon
"windows-x86_64" # Windows 64-bit
]
# Environment variables
[env]
LOG_LEVEL = "info"
API_ENDPOINT = "https://api.example.com"
# Shutdown configuration
[stop]
grace_seconds = 30 # Time to gracefully shutdown (1-300 seconds)
# Marketplace metadata
homepage = "https://acme.com/canbus"
keywords = ["can", "automotive", "dbc", "j1939"] # Max 20
categories = ["Automotive", "IoT"] # Max 10
# File references (all relative paths)
icon = "assets/icon.png"
readme = "README.md"
changelog = "CHANGELOG.md"
# Compatibility
# Configuration schema
[config]
schema = "schemas/config.json" # JSON Schema for validation
Manifest Field Reference¶
Required Fields¶
All Required Fields Must Be Present
Missing any required field will prevent your extension from being published or installed.
| Field | Type | Description | Validation |
|---|---|---|---|
name |
string |
Display name | • Cannot be empty or whitespace • Shows in marketplace and app • Example: "Data Processor Pro" |
version |
string |
Semantic version | • Strict SemVer format • Example: 1.2.3 or 2.0.0-beta.1• Used for updates and compatibility |
runtime |
object |
Execution config | • See Runtime Configuration • Defines how extension runs |
Where does the extension ID come from?
You do not need to specify an internal id in the manifest. For marketplace installs, Zelos derives the ID from your GitHub repository in the form owner.repo (lowercased, with punctuation normalized). For local development installs, the ID defaults to local.<foldername>.
Optional Fields¶
| Field | Type | Default | Description |
|---|---|---|---|
description |
string |
- | Optional summary shown in marketplace |
author |
string |
- | Optional author or organization |
zelos.version |
string |
^25.0.20 |
Zelos compatibility • NPM-style SemVer range • Examples: ^25.0.20, >=25.0.0, *• Checked at install time |
repository |
string |
- | GitHub URL • Must be valid HTTPS URL • Example: https://github.com/org/repo• Used for marketplace integration |
workdir |
string |
"." |
Working directory for execution • Top-level field (not under runtime) • Relative to extension root • No .. or hidden directories |
targets |
array |
[] (all) |
Supported platforms • Empty = all platforms • See Platform Targets |
env |
object |
{} |
Environment variables • Passed to extension process • Reserved keys prohibited |
stop.grace_seconds |
integer |
10 |
Graceful shutdown time • Range: 1-300 seconds • Process receives SIGTERM |
keywords |
array |
[] |
Search terms (max 20) • Used for marketplace discovery • No empty strings allowed |
categories |
array |
[] |
Marketplace categories (max 10) • Examples: "IoT", "Automotive" • No empty strings allowed |
homepage |
string |
- | Extension website • Must be valid URL • Shown in marketplace |
icon |
string |
- | Path to icon file • Relative path in archive • Optional |
readme |
string |
- | Path to README • Markdown format • Optional |
changelog |
string |
- | Path to CHANGELOG • Version history • Optional |
config.schema |
string |
"config.schema.json" |
Path to JSON Schema • Validates user configuration • Default: config.schema.json |
Version Compatibility¶
Zelos uses semantic versioning to ensure compatibility between extensions and the platform.
The zelos.version field specifies which Zelos versions your extension is compatible with:
Common version patterns:
^25.0.20- Compatible with 25.x.x (recommended for most extensions)>=25.0.0- Version 25.0.0 or higher>=25.0.0, <26.0.0- Between 25.0.0 and 26.0.0 (exclusive upper bound)~25.0.20- Compatible with 25.0.x only*- Compatible with any version (explicit wildcard)- Omit the field entirely - Defaults to
^25.0.20(current version)
Version Checking
Version requirements are checked at installation time. If the user's Zelos version doesn't meet the requirements, the installation will fail with a clear error message.
Platform Targets¶
Specify which platforms your extension supports using target strings:
| Target String | OS | Architecture | Description |
|---|---|---|---|
linux-x86_64 |
Linux | x86-64 | Standard Linux desktop/server |
linux-aarch64 |
Linux | ARM64 | Raspberry Pi, embedded Linux |
darwin-x86_64 |
macOS | Intel | Older Macs (pre-2020) |
darwin-arm64 |
macOS | Apple Silicon | M1/M2/M3 Macs |
windows-x86_64 |
Windows | x86-64 | Windows 10/11 |
Universal Extensions
Leave targets empty or omit it entirely to support ALL platforms. This is ideal for pure Python extensions.
Platform-Specific Packaging¶
When creating platform-specific releases:
-
Name your archives with platform identifiers:
-
The marketplace automatically detects platforms from filenames:
- Keywords:
linux,darwin/macos,windows - Architectures:
x86_64/amd64,arm64/aarch64 - Universal:
any,universal, or no platform keywords
- Keywords:
-
Installation priority:
- Exact platform match
- Universal package
- Error if no compatible package
How Platform Detection Works¶
The marketplace automatically detects platform targets from archive filenames using keyword matching:
Operating System Detection:
- Keywords
darwin,macos,osx→macos - Keyword
linux→linux - Keywords
win32,windows,.exe→windows
Architecture Detection:
- Keywords
x64,x86_64,amd64→x86_64 - Keywords
arm64,aarch64→arm64(macOS) oraarch64(Linux) - Keywords
arm,armv7→arm
Fallback Behavior:
- If both OS and architecture detected → specific platform target
- If neither detected → universal package (
{os: "any", arch: "any"}) - If only one detected → treated as universal
Examples:
my-ext-1.0.0-linux-x86_64.tar.gz → linux-x86_64
my-ext-1.0.0-macos-arm64.tar.gz → darwin-arm64
my-ext-1.0.0-universal.tar.gz → universal (any/any)
my-ext-1.0.0.tar.gz → universal (no keywords)
Case Insensitive
All keyword matching is case-insensitive. Linux, LINUX, and linux are equivalent.
Runtime Configuration¶
Python Runtime¶
Python is the recommended runtime for most extensions, providing a rich ecosystem and easy development.
Basic Configuration¶
| Field | Required | Description |
|---|---|---|
type |
Yes | Must be "python" |
entry |
Yes | Path to your Python entry point (e.g., "main.py") |
python_version |
No | Python version in major.minor format (default: "3.11") |
Python Version Support¶
Zelos uses UV for Python environment management. UV automatically downloads the Python version you specify - no pre-installation needed!
Python Version Requirement
The zelos-sdk requires Python ≥3.10. Extensions specifying Python <3.10 will install successfully but fail at runtime with import errors. Always use Python 3.10 or higher.
How It Works
When a user installs your extension, UV automatically:
- Downloads the specified Python version if not present
- Creates an isolated virtual environment
- Installs your dependencies
- Runs your extension
This ensures consistent behavior across all platforms and users.
Virtual Environment Details¶
Every Python extension runs in an isolated virtual environment:
- Tool: Uses
uv(ultra-fast Python package manager) - Location:
.venv/in extension directory - Dependencies: Installed from
pyproject.tomlanduv.lock - Cache:
.cache/directory for faster reinstalls - Timeout: 15 minutes for dependency installation
- Isolation: No access to system Python packages
Extension Entry Point Patterns¶
The Cookiecutter template provides a production-ready entry point with proper structure, logging, and signal handling. Here are the core patterns:
The template uses a class-based approach with proper lifecycle management:
#!/usr/bin/env python3
import logging
import signal
import sys
from types import FrameType
import zelos_sdk
from zelos_sdk.hooks.logging import TraceLoggingHandler
from zelos_sdk.extensions import load_config
from my_extension.extension import MyExtension
# Configure basic logging before SDK initialization
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
# Initialize SDK (required before adding trace logging handler)
zelos_sdk.init(name="my_extension", actions=True)
# Add trace logging handler to send logs to Zelos
handler = TraceLoggingHandler("my_extension_logger")
logging.getLogger().addHandler(handler)
# Load configuration from config.json (with schema defaults applied)
config = load_config()
# Create extension and register actions
extension = MyExtension(config)
zelos_sdk.actions_registry.register(extension)
def shutdown_handler(signum: int, frame: FrameType | None) -> None:
"""Handle graceful shutdown."""
logger.info("Shutting down...")
extension.stop()
sys.exit(0)
signal.signal(signal.SIGTERM, shutdown_handler)
signal.signal(signal.SIGINT, shutdown_handler)
if __name__ == "__main__":
logger.info("Starting extension")
extension.start()
extension.run()
Benefits: - Proper separation of concerns - Testable code structure - Built-in logging integration - Graceful shutdown handling - Action registration support
Most extensions don't need async
The template uses a simpler synchronous pattern that works for most use cases. Only use async if you have specific async I/O requirements (e.g., async HTTP clients, websockets).
For async operations, you can use this pattern:
#!/usr/bin/env python3
import asyncio
import logging
import signal
import sys
from types import FrameType
import zelos_sdk
logger = logging.getLogger(__name__)
class AsyncExtension:
def __init__(self):
self.running = False
self._shutdown = asyncio.Event()
self.source = zelos_sdk.TraceSource("data")
self.source.add_event(
"event",
[
zelos_sdk.TraceEventFieldMetadata("value", zelos_sdk.DataType.Float64),
],
)
def start(self) -> None:
self.running = True
def stop(self) -> None:
self.running = False
self._shutdown.set()
async def run_async(self) -> None:
while self.running:
try:
# Your async logic here
self.source.event.log(value=42.0)
# Wait with cancellation support
try:
await asyncio.wait_for(self._shutdown.wait(), timeout=1.0)
except asyncio.TimeoutError:
continue
except asyncio.CancelledError:
break
extension = AsyncExtension()
def shutdown_handler(signum: int, frame: FrameType | None) -> None:
extension.stop()
signal.signal(signal.SIGTERM, shutdown_handler)
signal.signal(signal.SIGINT, shutdown_handler)
if __name__ == "__main__":
extension.start()
asyncio.run(extension.run_async())
Use the Template
The Cookiecutter template generates the proper structure automatically. These patterns are shown for reference when customizing your extension.
Managing Dependencies¶
The Cookiecutter template uses a simple, opinionated dependency workflow:
- Define dependencies in
pyproject.toml:
[project]
name = "my-extension"
version = "1.0.0"
dependencies = [
"zelos-sdk>=0.0.6",
"aiohttp>=3.9.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.4.2",
"pre-commit>=4.3.0",
"ruff>=0.13.3",
]
- Lock dependencies with
uv sync:
This ensures reproducible installations across all environments.
What gets included in your extension archive:
- ✅
pyproject.toml- Dependency specification (used during installation) - ✅
uv.lock- Locked dependency versions (ensures reproducible installs) - ✅ Python packages / modules referenced by
entry - ✅ Documentation assets you reference in
extension.toml(e.g., README, changelog, icon)
Template Handles This
The Cookiecutter template's just package command automatically bundles the correct files.
Environment Variables¶
Extensions receive a minimal, secure environment:
| Variable | Description | Source |
|---|---|---|
HOME |
User home directory | System |
PATH |
Minimal system PATH | System (filtered) |
TMPDIR/TEMP |
Temporary directory | System |
ZELOS_* |
Reserved for Zelos | Agent |
| Custom | Your variables | Manifest [env] |
Reserved Variables
Variables starting with ZELOS_ are reserved and cannot be set in the manifest.
Publishing Your Extension¶
The Cookiecutter template handles the entire release workflow for you.
Release Process¶
-
Create a release using the template's command:
-
Push to GitHub:
-
GitHub Actions automatically:
- Validates version matches the tag
- Runs all checks and tests
- Packages the extension as a tarball
- Creates a GitHub release with the archive attached
-
Submit to marketplace:
- Visit Zelos Marketplace
- Click "Submit Extension"
- Enter your repository URL
- Install the GitHub App (one-time, read-only)
- Wait for approval
After approval, users can install your extension directly from the Zelos App.
The Template Does It All
The Cookiecutter template includes:
just releasecommand that handles versioning- GitHub Actions workflow (
.github/workflows/release.yml) for automation - Packaging script that creates proper tarballs
- Version verification to prevent mismatches
You don't need to create archives manually or write workflows - it's all included.
Extension Configuration¶
Extensions can accept user configuration through a JSON config file. The Zelos app automatically generates a configuration UI with custom widgets, validation, and help text.
Quick Start¶
1. Create config.schema.json:
{
"type": "object",
"properties": {
"apiKey": {
"type": "string",
"title": "API Key",
"description": "Your API key for authentication",
"minLength": 32,
"ui:widget": "password",
"ui:placeholder": "Enter your API key..."
},
"interval": {
"type": "number",
"title": "Update Interval",
"description": "How often to check for updates (seconds)",
"minimum": 1,
"maximum": 3600,
"default": 60,
"ui:widget": "slider"
},
"enabled": {
"type": "boolean",
"title": "Enable Feature",
"default": true,
"ui:widget": "toggle"
}
},
"required": [
"apiKey"
]
}
2. Reference in extension.toml:
The Zelos app generates a configuration form from your schema. When users save their configuration, it's written to config.json in your extension's directory.
Schema Defaults
You can extract defaults from your schema to avoid duplicating them in code. The Cookiecutter template includes this helper.
Schema Validation During Publication
Important: The marketplace validates your config.schema.json during release ingestion, before users can install your extension.
What's validated: - Must be valid JSON - Must be valid JSON Schema (Draft 2020-12) - Schema file must exist at the path specified in manifest - Schema must be a JSON object with standard properties
If validation fails: Your extension will not be published to the marketplace. Test your schema locally before creating a release:
UI Customization Properties¶
Embed these ui:* properties in your schema to customize the form:
| Property | Description | Example |
|---|---|---|
ui:widget |
Choose widget type | "slider", "toggle", "password", "textarea", "radio" |
ui:help |
Tooltip with info icon | "This setting controls update frequency" |
ui:placeholder |
Input placeholder text | "Enter API key..." |
ui:rows |
Textarea rows (default: 4) | 8 |
ui:inline |
Display radio buttons inline | true |
Widget Names
Widget names are case-insensitive and support aliases:
Recommended (lowercase):
- "toggle" or "switch" → Toggle switch widget
- "range" or "slider" → Slider widget
- "password" → Password input
- "textarea" → Multi-line text
- "radio" → Radio button group
- "select" or "dropdown" → Dropdown menu
For consistency, use lowercase names in your schemas.
Widget Reference¶
Choose the right widget for your configuration fields:
Text Input Widgets¶
TextWidget (default for strings)
- Single-line text input
- No
ui:widgetneeded - automatic fortype: "string"
TextareaWidget
- Multi-line text input
- Use for longer text content
{
"description": {
"type": "string",
"title": "Description",
"ui:widget": "textarea",
"ui:rows": 6,
"ui:placeholder": "Enter detailed description..."
}
}
Password Widget
- Hidden password input
- Use for sensitive data
{
"password": {
"type": "string",
"title": "Password",
"minLength": 8,
"ui:widget": "password",
"ui:help": "Minimum 8 characters"
}
}
EmailWidget
- Email input with validation
- Automatic with
format: "email"
URLWidget
- URL input with validation
- Automatic with
format: "uri"
Numeric Widgets¶
NumberWidget (default for numbers)
- Number input with step controls
- Supports
minimum,maximum,multipleOf
{
"timeout": {
"type": "integer",
"title": "Timeout (seconds)",
"minimum": 1,
"maximum": 300,
"default": 30
}
}
Range Widget
- Slider for numeric values
- Requires
minimumandmaximum - Use
multipleOfto control step increments
{
"volume": {
"type": "number",
"title": "Volume",
"minimum": 0,
"maximum": 100,
"default": 75,
"ui:widget": "range"
}
}
Range with precise increments:
{
"interval": {
"type": "number",
"title": "Sample Interval (seconds)",
"description": "Data collection interval from 1kHz to 1Hz",
"minimum": 0.001,
"maximum": 1.0,
"multipleOf": 0.001,
"default": 0.1,
"ui:widget": "range",
"ui:help": "Slider steps in 1ms increments"
}
}
Boolean Widgets¶
CheckboxWidget (default for booleans)
- Standard checkbox
- No
ui:widgetneeded
Toggle Widget
- Toggle switch (better UX)
- Recommended for boolean settings
{
"enabled": {
"type": "boolean",
"title": "Enable Feature",
"default": true,
"ui:widget": "toggle"
}
}
Selection Widgets¶
SelectWidget (default for enums)
- Dropdown select
- Automatic for
enumarrays
{
"mode": {
"type": "string",
"title": "Mode",
"enum": [
"auto",
"manual",
"scheduled"
],
"default": "auto"
}
}
RadioWidget
- Radio button group
- Better for 2-5 options
{
"priority": {
"type": "string",
"title": "Priority",
"enum": [
"low",
"normal",
"high"
],
"default": "normal",
"ui:widget": "radio",
"ui:inline": true
}
}
MultiSelectWidget
- Multi-select dropdown with search
- Automatic for arrays with enum items
{
"features": {
"type": "array",
"title": "Enabled Features",
"items": {
"type": "string",
"enum": [
"logging",
"metrics",
"alerts",
"reports"
]
},
"uniqueItems": true
}
}
File Widgets¶
FilePathPickerWidget
- Opens file browser, returns path as string
- Does not upload the file
- Use for remote file paths or when file upload isn't needed
{
"bitstreamPath": {
"type": "string",
"title": "Bitstream File",
"ui:widget": "file-picker",
"ui:placeholder": "C:/path/to/file.bit"
}
}
FileWidget
- Single file upload
- Returns base64 data URL
- 10MB limit
{
"configFile": {
"type": "string",
"title": "Configuration File",
"format": "data-url",
"ui:help": "Upload JSON or YAML config file"
}
}
FilesWidget
- Multiple file upload
- Returns array of data URLs
- 50MB total limit
{
"logFiles": {
"type": "array",
"title": "Log Files",
"items": {
"type": "string",
"format": "data-url"
}
}
}
Processing uploaded files:
import base64
from zelos_sdk.extensions import load_config
config = load_config()
file_data_url = config.get("configFile")
if file_data_url and file_data_url.startswith("data:"):
# Split: "data:application/json;base64,SGVsbG8="
header, encoded = file_data_url.split(",", 1)
mime_type = header.split(";")[0].replace("data:", "")
# Decode base64
file_bytes = base64.b64decode(encoded)
# Use file
with open("uploaded_config.json", "wb") as f:
f.write(file_bytes)
Date/Time Widgets¶
DateWidget
- Calendar date picker
- Automatic with
format: "date"
Configuration Loading¶
When users save configuration in the Zelos app, it's written to config.json in your extension's working directory.
Loading Configuration¶
Use the SDK's load_config() helper to load configuration with schema defaults:
from zelos_sdk.extensions import load_config
# Load configuration from config.json (with schema defaults applied)
config = load_config()
# Access configuration values
interval = config.get("interval") # Gets default from schema if not in config.json
sensor_name = config.get("sensor_name")
What load_config() does:
- Loads config.json from your extension directory
- Applies default values from your config.schema.json
- Validates configuration against your schema
- Returns a merged dictionary with all defaults filled in
Benefits: - Automatic defaults - No need to hardcode default values - Schema validation - Catches configuration errors early - Simple API - One function call, everything works
Common Patterns¶
API Configuration¶
{
"type": "object",
"properties": {
"apiUrl": {
"type": "string",
"title": "API URL",
"format": "uri",
"default": "https://api.example.com"
},
"apiKey": {
"type": "string",
"title": "API Key",
"minLength": 32,
"ui:widget": "password"
},
"timeout": {
"type": "integer",
"title": "Timeout (seconds)",
"minimum": 1,
"maximum": 300,
"default": 30
}
},
"required": [
"apiUrl",
"apiKey"
]
}
Nested Configuration¶
{
"type": "object",
"properties": {
"connection": {
"type": "object",
"title": "Connection Settings",
"properties": {
"host": {
"type": "string",
"default": "localhost"
},
"port": {
"type": "integer",
"minimum": 1,
"maximum": 65535,
"default": 5000
}
}
}
}
}
Dynamic Arrays¶
{
"endpoints": {
"type": "array",
"title": "API Endpoints",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"title": "Name"
},
"url": {
"type": "string",
"title": "URL",
"format": "uri"
}
},
"required": [
"name",
"url"
]
},
"minItems": 1
}
}
Multi-Event Monitoring¶
For extensions that collect different types of data, organize events by category:
# Define multiple event types with proper types and units
def _define_schema(self) -> None:
# Environmental sensors - temperature and humidity
self.source.add_event(
"environmental",
[
zelos_sdk.TraceEventFieldMetadata("temperature", zelos_sdk.DataType.Float32, "°C"),
zelos_sdk.TraceEventFieldMetadata("humidity", zelos_sdk.DataType.Float32, "%"),
],
)
# Power readings - voltage and current
self.source.add_event(
"power",
[
zelos_sdk.TraceEventFieldMetadata("voltage", zelos_sdk.DataType.Float32, "V"),
zelos_sdk.TraceEventFieldMetadata("current", zelos_sdk.DataType.Float32, "A"),
],
)
# Log to different events
self.source.environmental.log(temperature=22.5, humidity=55.0)
self.source.power.log(voltage=12.0, current=2.5)
Benefits: - Clean separation of different data types - Users can plot/analyze each category independently - Easier to understand and maintain
Best Practices¶
- Provide defaults - Set sensible
defaultvalues for all optional fields - Add descriptions - Use
descriptionfor inline help,ui:helpfor detailed tooltips - Validate inputs - Use
minLength,maximum,pattern, etc. - Group related settings - Use nested objects for organization
- Choose appropriate widgets - Toggle switches for booleans, radio for few options
- Test your schema - Install your extension and verify the form renders correctly
Validation¶
The Zelos app validates configuration against your schema:
- Required fields - Marked with asterisk (*), validation on submit
- Type checking - Ensures correct data types
- Range validation - Checks minimum/maximum values
- Pattern matching - Validates regex patterns
- Custom formats - email, uri, date validated automatically
Validation errors appear inline below each field.
Troubleshooting¶
Widget not appearing:
- Verify
typematches widget requirements (e.g.,rangeneedstype: "number") - For
range: must have bothminimumandmaximumproperties - For
radio: must haveenumarray with options - Widget names are case-insensitive:
"toggle","Toggle","ToggleWidget"all work
File uploads not working:
- Use
"format": "data-url"(not"file") - Single file:
type: "string"+ format - Multiple files:
type: "array",items: { type: "string", format: "data-url" }
Validation errors:
- Check JSON syntax with a validator
- Verify all required fields have defaults or are explicitly required
- Test your schema at rjsf-team.github.io/react-jsonschema-form
Important Notes
- Widget names are case-insensitive -
"range","Range","RangeWidget"all work - Common aliases supported -
"slider"→"range","switch"→"toggle" ui:*properties are frontend-only - Backend validation ignores them (safe to use)- File size limits - 10MB single file, 50MB total for multiple files
- Base64 encoding - File uploads are base64-encoded data URLs
Installation Details¶
Where Extensions Are Installed¶
Extensions are installed in platform-specific directories:
| Platform | Location |
|---|---|
| Linux | ~/.local/share/io.zeloscloud.zelos/extensions/{id}/{version}/ |
| macOS | ~/Library/Application Support/io.zeloscloud.zelos/extensions/{id}/{version}/ |
| Windows | %LOCALAPPDATA%\io.zeloscloud.zelos\extensions\{id}\{version}\ |
Installation Directory Structure¶
acme.sensor-monitor/
└── 1.0.0/
├── extension.toml # Manifest
├── main.py # Your code
├── config.json # User configuration
├── extension.log # Runtime logs
├── .venv/ # Python virtual environment
├── .cache/ # Package cache
└── .last_config # Backup configuration
Installation Process¶
The Zelos Agent handles installation automatically:
- Download - Fetch archive from marketplace (60s timeout)
- Verify - Check SHA-256 checksum
- Extract - Unpack with security validation
- Prepare - Set up runtime environment
- Configure - Apply user configuration
- Start - Launch the extension
Security Measures¶
During installation, the system enforces:
- ✅ Path validation - No escaping installation directory
- ✅ Size limits - 500 MiB compressed, 2 GiB extracted
- ✅ File limits - Maximum 10,000 files
- ✅ No symlinks - Symbolic/hard links rejected
- ✅ Checksum verification - SHA-256 integrity check
Extension Lifecycle¶
Understanding the extension lifecycle helps you build robust, well-behaved extensions.
Shutdown Process¶
When your extension is stopped, the supervisor follows this precise sequence:
- Send SIGTERM to your extension process
- Poll status every 50 milliseconds to check if process exited
- Wait for up to
grace_seconds(default: 10, max: 300) - Force kill with SIGKILL if still running after grace period
- Confirm termination by waiting 1 second after SIGKILL
- Clean up process tracking (additional delays on Windows)
Timing Details¶
| Phase | Duration | Platform Notes |
|---|---|---|
| Poll interval | 50ms | Checks if process exited |
| Grace period | 1-300s (default: 10s) | Configurable in manifest |
| Force kill confirmation | 1s | After SIGKILL sent |
| Windows cleanup | 200ms × grace_seconds | Capped at 1000ms total |
Example Timeline¶
For an extension with grace_seconds = 30:
T+0ms: Send SIGTERM
T+50ms: Poll #1 - still running
T+100ms: Poll #2 - still running
...
T+30000ms: Grace period expired
T+30000ms: Send SIGKILL (cannot be caught)
T+31000ms: Confirm termination
T+31000ms: Extension fully stopped
Grace Period is a Hard Limit
If your extension doesn't exit within grace_seconds, it will be forcefully terminated with SIGKILL (which cannot be caught or ignored). Ensure your cleanup code completes within the grace period.
Without Signal Handling¶
If you don't handle SIGTERM, your extension will be forcefully terminated after the grace period. This is acceptable for:
- Simple, stateless extensions
- Prototypes and testing
- Extensions with no cleanup requirements
With Signal Handling (Recommended)¶
For production extensions, handle SIGTERM to shut down gracefully:
import signal
import sys
def cleanup():
# Save state
# Close connections
# Flush buffers
pass
def signal_handler(signum, frame):
print(f"Received signal {signum}, shutting down...")
cleanup()
sys.exit(0)
# Register handlers
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
Configuring Grace Period¶
Adjust the shutdown timeout in your manifest based on your cleanup needs:
Grace Period Behavior
If your extension doesn't exit within grace_seconds, it will be force-killed with SIGKILL (cannot be caught). Make sure your cleanup code completes within the grace period.
Updates and Compatibility¶
When users update your extension:
- Old version receives
SIGTERMshutdown signal - Configuration is preserved
- New version starts with saved config
- Rollback occurs if startup fails
Security Best Practices¶
Path Security¶
The extension system enforces strict path security:
✅ DO:
- Use relative paths only
- Use forward slashes (
/) - Keep files within your archive
❌ DON'T:
- Use absolute paths (
/etc/passwd) - Use parent references (
../../../) - Include hidden files (
.git/) - Create symbolic links
Process Isolation¶
Your extension runs in a sandboxed environment:
| Isolation Level | Description |
|---|---|
| Process | Separate process with limited privileges |
| File System | Confined to extension directory |
| Environment | Minimal, sanitized variables |
| Network | No restrictions (use responsibly) |
Security Checklist¶
Before publishing, ensure:
- No hardcoded secrets or API keys
- Input validation for all external data
- Proper error handling without exposing internals
- Dependencies from trusted sources only
- No execution of user-provided code
- Graceful handling of resource limits
- Regular security updates for dependencies
Error Handling¶
Common Errors and Solutions¶
| Error | Cause | Solution |
|---|---|---|
| Download timeout | Large archive or slow connection | Reduce archive size, optimize dependencies |
| Checksum mismatch | Corrupted download | Re-upload release assets |
| Invalid manifest | Missing required fields | Validate extension.toml before publishing |
| Dependencies failed | Package installation error | Pin versions, test in clean environment |
| Config validation failed | Schema mismatch | Test schema with various configs |
Debugging Tips¶
- Check logs:
extension.login installation directory - Test locally: Run without Zelos Agent first
- Validate manifest: Ensure all paths and fields are correct
- Minimal dependencies: Start simple, add incrementally
- Use debug mode: Add verbose logging when needed
Development Best Practices¶
Design Principles¶
| Principle | Description |
|---|---|
| Single Responsibility | Do one thing well |
| Fail Gracefully | Handle errors without crashing |
| Resource Efficient | Minimize CPU, memory, and network usage |
| User Friendly | Clear logs, helpful error messages |
| Maintainable | Clean code, good documentation |
Code Organization¶
my-extension/
├── extension.toml # Manifest
├── pyproject.toml # Dependencies & metadata (recommended)
├── uv.lock # Lock file for reproducible builds (recommended)
├── main.py # Entry point
├── config.schema.json # JSON Schema for configuration UI
├── my_extension/ # Python package (importable modules)
│ ├── __init__.py
│ ├── extension.py # Main extension class with actions
│ ├── core.py # Core logic
│ └── utils/ # Your utility functions (if needed)
│ └── __init__.py
├── tests/ # Unit tests
│ └── test_extension.py
├── .vscode/ # VSCode integration
│ ├── settings.json # Auto-formatting, testing, etc.
│ └── extensions.json # Recommended extensions (Ruff, Python, etc.)
├── .github/
│ ├── workflows/
│ │ ├── CI.yml # Continuous Integration
│ │ └── release.yml # Automated releases
│ └── dependabot.yml # Automated dependency updates
├── Justfile # Task runner (install/check/test/package)
└── README.md # User documentation
Commit uv.lock and pyproject.toml
Always commit both uv.lock and pyproject.toml to your repository. This ensures:
- Reproducible installs for collaborators and CI
- The marketplace archive includes both files for end users
- Consistent dependency versions across all environments
Testing Your Extension¶
Local Testing¶
The template's just commands make testing simple:
# Install everything
just install
# Run checks
just check # Linting + type checking
just test # Run test suite
# Run extension locally
just dev
# Test with different configs
echo '{"sensor_name": "test", "interval": 0.5}' > config.json
just dev
All dependencies are managed by UV automatically - no manual venv setup needed.
Refreshing Dependencies¶
After you modify pyproject.toml dependencies (using uv add or manually editing), tell the agent to
rebuild the virtual environment without uninstalling the extension:
# Recreate the venv and reinstall dependencies for a running local extension
zelos extensions reinstall local.my-extension
The reinstall command preserves your configuration, invalidates the cached environment, and reapplies the integrity checks used for marketplace extensions.
Configuration lives outside your code tree
When the agent starts an extension it writes the active configuration to
<install-dir>/config.json and exposes that path via the
ZELOS_CONFIG_PATH environment variable. The default
zelos_sdk.extensions.load_config() helper automatically uses that variable.
You can trigger the same operation from the Zelos App by opening the extension detail pane and clicking the Reinstall button. The agent stops you from running the rebuild while the extension is still executing, so stop it first if you need to refresh dependencies.
Troubleshooting Guide¶
Common Issues¶
| Problem | Solution |
|---|---|
| Extension not appearing in marketplace | Ensure extension.toml is in repo root and release has assets |
| Installation timeout | Reduce dependency size, use lightweight packages |
| Extension won't start | Check extension.log for errors |
| Configuration rejected | Validate JSON and match schema |
| Platform not supported | Add platform to targets array |
Getting Help¶
- Check the logs - Find
extension.login installation directory - Validate locally - Test extension before publishing
- Review examples - Compare with working extensions