Extension Configuration¶
An agent extension can take user configuration. You describe it with a JSON Schema file, config.schema.json. The Zelos App turns that schema into a configuration form with widgets, validation, and help text. Your code reads the saved values with load_config().
How Configuration Flows¶
-
You ship
config.schema.jsonand name it inextension.toml: -
A user opens Configure on the installed extension. The form is built from your schema. It starts from the last saved configuration for that agent when one exists, otherwise from the
defaultvalues in your schema. See Extensions for the user's side. - The user submits the form. Submitting starts a stopped extension or restarts a running one.
- The agent validates the configuration against your schema. Invalid configuration stops the start, and a running extension keeps running. A start with no configuration at all skips this check.
- The agent writes the configuration to
config.jsonin the extension's install directory and passes its path inZELOS_CONFIG_PATH. - Your code calls
load_config(), which fills in schema defaults and validates again.
From the CLI, pass configuration with zelos extensions start <id> --config '<json>' or --config-file <path>. zelos extensions config <id> prints the schema and the last saved configuration.
Design default, required, description, and the ui:* hints for that first-run and edit workflow, not for raw JSON editing.
Where the Schema Is Read¶
| Reader | What it reads |
|---|---|
| Agent (on start and restart) | [config].schema when set. Otherwise config.schema.json in the code directory, if it exists. A declared path that does not exist is an error. |
zelos extensions package |
The [config].schema file, plus anything under [package].paths. |
| Marketplace (at release ingestion) | The [config].schema file, only when declared. |
load_config() in your code |
config.schema.json in the working directory, unless you pass schema_path=. |
Always declare [config]
The agent finds config.schema.json without [config] during local development. A packaged release leaves the file out unless [config] names it or a [package] path covers it. Declare [config] so local and released behavior match.
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 it in extension.toml:
3. Read it in your extension:
from zelos_sdk.extensions import load_config
config = load_config()
interval = config["interval"] # 60 unless the user changed it
Loading Configuration¶
load_config() loads, completes, and validates the configuration:
What it does:
- Reads the configuration from
ZELOS_CONFIG_PATH. Without that variable, it readsconfig.jsonin the working directory. - Reads the schema from
config.schema.jsonin the working directory. - Fills in
defaultvalues from the schema, including nested objects,$reftargets, and array items up tominItems. - Validates the result against the schema and raises
ConfigValidationErroron failure. - Returns a plain
dict.
A missing configuration file reads as {}. A missing schema file skips defaults and validation.
Optional arguments:
| Argument | Default | Use |
|---|---|---|
config_path |
"config.json" |
Read a different configuration file. ZELOS_CONFIG_PATH applies only while this is the default. |
schema_path |
"config.schema.json" |
Read the schema from another path. Pass it when [config].schema is not config.schema.json or workdir is not the project root. |
overrides |
None |
A dict deep-merged over the result, then validated again. |
ConfigValidationError has errors, a list of messages, and field_errors, a dict from field path to message.
When you run the extension yourself with just dev, put a config.json next to main.py to test with specific values.
Schema Validation¶
The agent and load_config() validate with a standard JSON Schema validator. The marketplace checks that the schema is valid JSON and a valid JSON Schema (Draft 2020-12) before it publishes a release. A release with an invalid declared schema is not published.
ui:* keys are form hints. Validators ignore them.
Check your schema before you release:
Then open Configure in the app and submit the form once.
UI Hints¶
Add these ui:* keys to a property to shape its field:
| Key | Applies to | Effect |
|---|---|---|
ui:widget |
Any field | Pick a widget. See Widgets. |
ui:help |
Any field | Help text in a tooltip. Without it, description is used. |
ui:placeholder |
Text and number inputs | Placeholder text. |
ui:rows |
textarea |
Visible rows. Default 4. |
ui:inline |
radio |
Lay out the options on one line. |
ui:showValue |
range |
false hides the current value next to the slider. |
ui:encoding |
file, files |
dataurl (default), base64 (no data: prefix), or text (file contents as text). |
ui:maxFiles |
files |
Most files a user can pick. Default 10. |
ui:hideLabel |
Any field | Hide the field's label. |
ui:options |
Depends on widget | Widget options, for example accept, createDirectory, collapsed, action, autoconfig. |
On an object property, "ui:options": {"collapsed": true} starts the group closed behind its title.
Widgets¶
Each JSON type has a default widget. Set ui:widget to pick another. Use the lowercase names below. A widget name the app does not know falls back to the default widget for that field.
ui:widget value |
Widget | Default for |
|---|---|---|
text |
Single-line text input | type: "string", "number", and "integer" |
textarea |
Multi-line text | - |
password |
Hidden text | - |
email |
Email input | format: "email" |
url |
URL input | format: "uri" |
number, updown |
Number input with step controls | - |
range, slider |
Slider | - |
checkbox |
Checkbox | type: "boolean" |
toggle, switch |
Toggle switch | - |
select, dropdown |
Dropdown | enum |
radio |
Radio buttons | - |
multiselect |
Searchable multi-select | - |
file |
Single file upload | format: "data-url" |
files |
Multiple file upload | Array of format: "data-url" items |
file-picker, filepicker, pathpicker |
File path picker | - |
folder-picker, folderpicker, directory-picker, directorypicker |
Folder path picker | - |
action-choices |
Text input with choices from one of your actions | - |
date |
Date picker | format: "date" |
datetime |
Date and time picker | format: "date-time" |
time |
Time picker | format: "time" |
Text¶
{
"notes": {
"type": "string",
"title": "Notes",
"ui:widget": "textarea",
"ui:rows": 6,
"ui:placeholder": "Enter a detailed description..."
},
"password": {
"type": "string",
"title": "Password",
"minLength": 8,
"ui:widget": "password",
"ui:help": "Minimum 8 characters"
},
"endpoint": {
"type": "string",
"title": "API Endpoint",
"format": "uri"
}
}
Numbers¶
A number field without ui:widget is a text input that accepts numbers. "ui:widget": "number" adds step controls that step by multipleOf when set.
range draws a slider. It needs minimum and maximum. multipleOf sets the step size:
{
"interval": {
"type": "number",
"title": "Sample Interval (seconds)",
"description": "Data collection interval from 1 kHz to 1 Hz",
"minimum": 0.001,
"maximum": 1.0,
"multipleOf": 0.001,
"default": 0.1,
"ui:widget": "range",
"ui:help": "Slider steps in 1 ms increments"
}
}
The agent accepts floating-point values that are within rounding error of a multipleOf step.
Booleans¶
{
"enabled": {
"type": "boolean",
"title": "Enable Feature",
"default": true,
"ui:widget": "toggle"
}
}
Choices¶
An enum renders as a dropdown. Use radio for two to five options:
{
"priority": {
"type": "string",
"title": "Priority",
"enum": ["low", "normal", "high"],
"default": "normal",
"ui:widget": "radio",
"ui:inline": true
}
}
For several values from a fixed list, use an array of enum items with "ui:widget": "multiselect":
{
"features": {
"type": "array",
"title": "Enabled Features",
"items": {
"type": "string",
"enum": ["logging", "metrics", "alerts", "reports"]
},
"uniqueItems": true,
"ui:widget": "multiselect"
}
}
Choices From an Action¶
action-choices fills a text field's suggestions from a standalone action in your own extension. The agent runs the action on the extension's host, even while the extension is stopped. Use it for values only that host can see, such as network interfaces or CAN buses:
{
"interface": {
"type": "string",
"title": "Interface",
"ui:widget": "action-choices",
"ui:options": { "action": "Packet/list_interfaces" }
}
}
ui:options.actionnames the action as<extension name>/<action path>. The form refuses an action from another extension.ui:options.paramsis passed to the action as its arguments.- The field stays a text input, so a user can type a value the agent cannot see.
For the action side, see Choices for a Config Field.
Auto-Configure¶
A root-level autoconfig option adds an Auto-configure button above the form. The button runs a standalone action in your extension, and the keys of the config object it returns replace the form's values. Nothing runs until the user clicks the button, and the user reviews the result before saving:
{
"type": "object",
"ui:options": { "autoconfig": "Packet/auto_config" },
"properties": {
"interfaces": { "type": "array", "items": { "type": "object" } }
}
}
The action must belong to the same extension. For the action side, see Auto-configure from an Action.
File and Folder Paths¶
file-picker and folder-picker open a native browser and store the chosen path as a string. They do not upload anything, so the path must make sense on the agent's machine.
{
"bitstreamPath": {
"type": "string",
"title": "Bitstream File",
"ui:widget": "file-picker",
"ui:placeholder": "C:/path/to/file.bit"
},
"outputDir": {
"type": "string",
"title": "Output Directory",
"ui:widget": "folder-picker",
"ui:options": { "createDirectory": true }
}
}
createDirectory lets the user create a new folder from the picker.
File Uploads¶
format: "data-url" uploads file contents into the configuration.
| Widget | Schema | Value | Size limit |
|---|---|---|---|
file |
type: "string", format: "data-url" |
One data URL | 10 MB |
files |
type: "array", items type: "string", format: "data-url" |
Array of data URLs | 50 MB total, 10 files unless ui:maxFiles |
{
"dbcFile": {
"type": "string",
"title": "DBC File",
"format": "data-url",
"ui:options": { "accept": ".dbc" }
},
"logFiles": {
"type": "array",
"title": "Log Files",
"items": { "type": "string", "format": "data-url" }
}
}
ui:options.accept takes a comma-separated list of extensions (.dbc) or MIME types. ui:encoding changes the stored value; see UI Hints.
Decode uploads in your extension and write them to ZELOS_DATA_DIR. The code directory is read-only.
import base64
import os
from pathlib import Path
from zelos_sdk.extensions import load_config
config = load_config()
data_url = config.get("dbcFile")
if data_url and data_url.startswith("data:"):
# "data:application/octet-stream;base64,SGVsbG8="
header, encoded = data_url.split(",", 1)
file_bytes = base64.b64decode(encoded)
data_dir = Path(os.environ["ZELOS_DATA_DIR"])
dbc_path = data_dir / "uploaded.dbc"
dbc_path.write_bytes(file_bytes)
Dates and Times¶
Common Patterns¶
API Settings¶
{
"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 Groups¶
{
"type": "object",
"properties": {
"connection": {
"type": "object",
"title": "Connection Settings",
"ui:options": { "collapsed": true },
"properties": {
"host": { "type": "string", "default": "localhost" },
"port": { "type": "integer", "minimum": 1, "maximum": 65535, "default": 5000 }
}
}
}
}
Lists of Objects¶
{
"endpoints": {
"type": "array",
"title": "API Endpoints",
"minItems": 1,
"items": {
"type": "object",
"properties": {
"name": { "type": "string", "title": "Name" },
"url": { "type": "string", "title": "URL", "format": "uri" }
},
"required": ["name", "url"]
}
}
}
Best Practices¶
- Give every optional field a
default, so a first start works without edits. - Use
descriptionfor one line of inline help andui:helpfor longer guidance. - Constrain inputs with
minLength,minimum,maximum,pattern, andenum. - Group related settings in nested objects.
- Pick widgets that match the data: toggles for booleans, radio buttons for a few options, pickers for paths.
- Install locally and open Configure to check the form before you release.
Troubleshooting¶
A widget does not appear:
- Check that
typefits the widget.rangeneedstype: "number"or"integer"plusminimumandmaximum.radioneeds anenum. - Use a lowercase name from the widget table. An unknown name falls back to the default widget.
A multi-value field saves one value:
- Add
"ui:widget": "multiselect"to the array property.
File uploads do not work:
- Use
"format": "data-url", not"format": "file". - One file:
type: "string"with the format. Several files:type: "array"withitems: {"type": "string", "format": "data-url"}.
Start fails with Configuration validation failed:
- The message lists each failing field path. Fix the value in Configure, or fix the schema if the default itself is invalid.
The extension starts, then exits with Configuration validation failed:
- It was started with no configuration, so the agent skipped validation and
load_config()raised inside the extension. Arequiredfield without a value is the usual cause. Start it from Configure, or pass--configor--config-file.
Start fails with Config schema file '<path>' not found in extension:
[config].schemanames a file that is not in the installed code. For a release, check that the archive contains it withzelos extensions package --list ..