Skip to content

Extension Runtime and Lifecycle

This page describes what the Zelos Agent does with an agent extension: how it installs it, the environment it runs in, how it starts and stops it, and where its files and logs live. App extensions run in the desktop app instead; see Develop App Extensions.

Install

From the Marketplace

  1. Download. The agent fetches the archive for its platform, with a 60-second timeout and a 2 GiB cap.
  2. Verify. It checks the SHA-256 checksum the marketplace recorded.
  3. Extract. It unpacks the archive and rejects links, special files, absolute or .. paths, and hidden entries. See Archive Rules and Limits.
  4. Check. It parses extension.toml, checks the [zelos] version range, and checks that the files the manifest names exist.
  5. Prepare. It moves the files to code/ and builds the Python environment. See Python Environment.
  6. Lock. It records an integrity hash in .integrity and makes code/ read-only.

The extension is then installed and stopped. A user starts it from the app or with zelos extensions start.

From a Local Directory

zelos extensions install-local <dir> installs your working copy for development:

  • The agent does not copy your code. It writes a .dev_source file that points at your project directory, and runs the code from there.
  • Runtime files (the virtual environment, cache, configuration, data, and logs) live in the install directory, outside your project.
  • The ID is local.<slug>, from the manifest name. See Extension Identity.
  • The agent builds the Python environment, then writes actions.json into your project. See actions.json.

Installing the same project again replaces the previous local install.

Python Environment

Every agent extension runs in its own virtual environment, built with uv:

  • uv downloads the Python version from python_version when needed. Zelos uses only uv-managed Python, never a system interpreter.
  • With a uv.lock and no requirements override, the agent runs uv sync --frozen for the exact locked versions.
  • With a requirements file, it creates the environment and runs uv pip sync, which installs exactly the listed set.
  • Otherwise it creates the environment and runs uv pip install against pyproject.toml.
  • Installed dependencies are compiled to bytecode, so the first start is faster.
  • The environment lives in .venv/ in the install directory, with a uv cache in .cache/.
  • Dependency installation times out after 15 minutes.

Python 3.10 or later

The manifest accepts any major.minor version, but zelos-sdk needs Python 3.10 or later. An extension that asks for an older Python installs and then fails at import.

Install Locations

Platform Location
macOS ~/Library/Application Support/io.zeloscloud.zelos/extensions/<id>/<version>/
Linux, desktop app ~/.local/share/zelos/extensions/<id>/<version>/
Linux, headless zelos-agent package /var/lib/zelos-agent/extensions/<id>/<version>/
Windows %LOCALAPPDATA%\zeloscloud\zelos\data\extensions\<id>\<version>\

When the agent runs with ZELOS_DATA_DIR set, extensions live under $ZELOS_DATA_DIR/extensions/.

acme.sensor-monitor/
└── 1.0.0/
    ├── code/             # Your shipped files, read-only (marketplace installs)
    │   ├── extension.toml
    │   ├── actions.json
    │   └── main.py
    ├── .integrity        # Integrity hash of code/ (marketplace installs)
    ├── .dev_source       # Path to your project (local installs, in place of code/)
    ├── config.json       # Last saved configuration
    ├── data/             # Writable storage, exposed as ZELOS_DATA_DIR
    ├── extension.log     # stdout and stderr
    ├── extension.log.1   # Previous log, after rotation
    ├── .venv/            # Python virtual environment
    └── .cache/           # uv cache

For a marketplace install, the agent checks the integrity hash before it starts the extension. A manual edit under code/ changes the hash, and the agent then refuses to start the extension.

Start

When an extension starts, the agent:

  1. Validates the configuration against the extension's schema. See Extension Configuration. Invalid configuration fails the start, and a running extension keeps running on a restart.
  2. Writes the configuration to config.json.
  3. Creates data/ if it does not exist.
  4. Runs the entry file with the environment's Python, in workdir under the code directory.
  5. Sends the process's stdout and stderr to extension.log.

Start, restart, stop, and reinstall apply to agent extensions only. For an app extension they fail with Operation not supported for app extensions.

Process Environment

The agent starts the process with an empty environment and adds back only these variables.

Variable Value
PATH, TEMP, TMP Inherited from the agent
HOME, USER, LOGNAME, LANG, LC_ALL, LC_CTYPE Inherited from the agent (macOS and Linux)
SystemRoot, USERPROFILE, APPDATA, LOCALAPPDATA Inherited from the agent (Windows)
SSL_CERT_FILE, REQUESTS_CA_BUNDLE Inherited from the agent, for custom CA certificates
HTTP_PROXY, HTTPS_PROXY, NO_PROXY Inherited from the agent, for proxies
TZ Inherited from the agent
Your env entries From [host.agent.env]
PYTHONUNBUFFERED 1, so log lines arrive without delay
PYTHONIOENCODING UTF-8
ZELOS_CONFIG_PATH Path to config.json
ZELOS_DATA_DIR Path to the writable data/ directory
ZELOS_AGENT_URL The URL of the agent that started the extension
ZELOS_STANDALONE 1, only in a standalone action run. See Standalone Action Runs.

Only the uppercase proxy names pass through. Variables that start with ZELOS_ are reserved, and the manifest cannot set them.

How the SDK uses these:

  • zelos_sdk.init() connects to ZELOS_AGENT_URL, so an extension reports to the agent that started it even when another agent holds port 2300. Without the variable it uses http://localhost:2300.
  • load_config() reads ZELOS_CONFIG_PATH. See Loading Configuration.

Files and Storage

The code directory is read-only for marketplace installs. Write files to ZELOS_DATA_DIR:

import os
from pathlib import Path

data_dir = Path(os.environ["ZELOS_DATA_DIR"])
(data_dir / "cache.json").write_text("{}")

data/ survives restarts, reinstalls, and updates. An update moves it to the new version.

Standalone Action Runs

The agent can run a standalone action while the extension is stopped, for example to fill a configuration form's choices. For each run it starts a separate short-lived Python process in the extension's environment:

  • The process gets the same environment as a supervised start, without ZELOS_AGENT_URL and with ZELOS_STANDALONE=1.
  • While ZELOS_STANDALONE is set, zelos_sdk.init() opens no agent connection, so the stopped extension does not appear live.
  • The process imports your entry module to find the action. Keep startup code under if __name__ == "__main__": so the import does not start your extension.

The list of standalone actions comes from actions.json. See actions.json and Choices for a Config Field.

Logs

The agent appends the process's stdout and stderr to extension.log. When the file passes 10 MiB at the next start, the agent renames it to extension.log.1, replacing any older rotation.

Read the log:

  • In the app, use View Logs on the extension.
  • From the CLI, use zelos extensions logs:
zelos extensions logs local.my-extension
zelos extensions logs local.my-extension --tail 65536
zelos extensions logs local.my-extension -o extension.log

--tail <BYTES> reads only the last bytes. Without it, the command reads the whole retained log, extension.log.1 first, up to a cap set by the agent. When the agent drops older output, the command says so on stderr. -o writes the log to a file.

Use TraceLoggingHandler to also send Python log records into Zelos as trace data. See Integrate Logging.

Stop and Shutdown

On macOS and Linux, the extension runs in its own process group. To stop it, the agent:

  1. Sends SIGTERM to the process group, so child processes receive it too.
  2. Checks every 50 ms whether the group has exited.
  3. Waits up to grace_seconds (default 10, range 1 to 300).
  4. Sends SIGKILL to the group if it is still running. SIGKILL cannot be caught.
  5. Waits up to 1 second for the kill to finish, then kills any descendants that left the group.

On Windows there is no SIGTERM. The extension runs in a job object. The agent waits grace_seconds × 200 ms, at most 1 second, then terminates the job with all its processes. Shutdown handlers do not run on Windows, so do not rely on them to save state there.

Set the grace period in the manifest:

[host.agent.stop]
grace_seconds = 30

Example timeline on macOS or Linux with grace_seconds = 30:

T+0 ms       SIGTERM sent to the process group
T+50 ms      Poll: still running
...
T+30000 ms   Grace period over: SIGKILL sent
T+31000 ms   Confirmation window over: extension stopped

Handle SIGTERM

Without a SIGTERM handler, Python exits at once and skips your cleanup. That is acceptable for stateless extensions. When you need to close connections or flush data, handle the signal and let your main loop end:

import signal
from types import FrameType


def shutdown_handler(signum: int, frame: FrameType | None) -> None:
    extension.stop()  # make the main loop exit, then clean up


signal.signal(signal.SIGTERM, shutdown_handler)
signal.signal(signal.SIGINT, shutdown_handler)

Finish cleanup inside grace_seconds. After that the agent kills the process.

Crashes and Exit Status

The agent records how an extension exited:

  • User-initiated stop. No exit information is recorded. The extension shows as stopped.
  • Unexpected exit (crash, signal, or error). The exit code or signal is recorded. The extension shows an error state, the app shows a notification, and View Logs opens the log.
Exit code or signal Meaning Typical cause
Code 0 Clean exit The main loop ended without a stop request
Code 1 General error Unhandled exception
Code 137 Killed Out of memory on Linux
Signal 9 (SIGKILL) Force-killed Out of memory, or an unresponsive process
Signal 11 (SIGSEGV) Segmentation fault A crash in native code
Signal 15 (SIGTERM) Terminated A stop request the process did not handle

Log meaningful errors before you exit. The log is what users see first after a crash.

Reinstall

zelos extensions reinstall <id> rebuilds the extension's Python environment without removing its configuration or data. Use it after you change dependencies. The app's Reinstall button does the same.

  • The agent refuses while the extension runs: Stop the extension before reinstalling. Stop it first.
  • For a marketplace install, the agent first checks code/ against its integrity hash. When the code has changed, it refuses and asks you to uninstall and install again from the marketplace.

Updates

An update installs the new version next to the old one, moves data/ and the saved configuration across, and restarts the extension if it was running. Any failure restores the old version. See Updates.

Security Model

Agent extensions:

  • Run as separate processes, as the same OS user as the agent. Zelos does not sandbox their file or network access.
  • Start from a minimal environment. See Process Environment.
  • Have a read-only code directory for marketplace installs, guarded by an integrity hash.
  • Must keep workdir inside the extension. The agent refuses a workdir that resolves outside it.

App extensions:

  • Run in an iframe sandboxed with allow-scripts allow-same-origin allow-downloads. They cannot submit forms or open popups.
  • Cannot reach the network. The Content Security Policy blocks fetch, XMLHttpRequest, and form submissions to http: and https: URLs.
  • Run under their own zelos-app://<extensionId> origin, isolated from the host and other extensions.
  • Load only the packaged files served by zelos-app://.

Before you publish, check that:

  • The code holds no secrets or API keys. Take them from configuration with the password widget.
  • The extension validates data from devices, files, and the network.
  • Error messages do not expose internals.
  • Dependencies come from trusted sources and are pinned in uv.lock.
  • The extension does not run code that users or devices supply.

Troubleshooting

Problem Fix
The extension does not start Run zelos extensions logs <id> and read the last error.
Configuration validation failed Fix the listed fields in Configure, or fix the schema defaults.
Extension requires Zelos version matching ... Update Zelos, or widen [zelos].version.
Dependency installation fails or times out Pin versions in uv.lock, test uv sync in a clean checkout, and trim large dependencies.
Stop the extension before reinstalling Run zelos extensions stop <id>, then reinstall.
The extension starts but sends no data Check that zelos_sdk.init() runs and that the log shows no connection errors.
A marketplace extension refuses to start after a manual edit Uninstall it and install it again from the marketplace. Files under code/ must not change.