Subprocess model
Every third-party agent plugin runs as its own process, started by systemd from a generated unit inside the sharedados-plugins.slice cgroup slice.
There is no in-process tier for third parties. Built-in plugins that declare
isolation: inprocess run inside the agent and skip everything on this page.
ados-plugin-host.service) owns the plugin’s Unix socket,
renders the unit, and is a hard dependency of every plugin unit.
Unit name
The unit name isados-plugin-<id with dots replaced by hyphens>-<8 hex>.service.
The 8 hex digits are the first 4 bytes of sha256 of the exact plugin id, so
two ids that sanitize to the same string (com.example.thermal and
com.example-thermal) never share a unit. com.example.thermal-lepton
becomes ados-plugin-com-example-thermal-lepton-1b3dd913.service.
Find yours on the node:
agent.contributes.services gets its
own unit, ados-plugin-<sanitized id>-<8 hex>-<service name>.service, in the
same slice and with the same sandbox as the main unit. Units are written to
/etc/systemd/system/.
Logs
The unit appends stdout and stderr to/var/log/ados/plugins/<sanitized id>-<8 hex>.log, for example
/var/log/ados/plugins/com-example-thermal-lepton-1b3dd913.log. A declared
service logs to <sanitized id>-<8 hex>-<service name>.log in the same
directory. Plugin output does not go to the journal.
ados plugin logs asks the agent for the plugin’s log path, so you never
build the name yourself:
--service reads a declared service’s log; an unknown service name exits
with the list of declared services. The supervisor trims any plugin log
past 32 MiB in place, keeping its tail.
Environment
The unit sets a small, fixed environment:
The capability token, agent id and data directory are not in the
environment. The host writes them to a root-owned
0600 file and the unit
loads it with LoadCredential=, so the process reads it as
$CREDENTIALS_DIRECTORY/ados-plugin-token, visible to that plugin’s user
alone. The host rewrites it on every start and every token rotation. The
Python runner reads it for you; a Rust plugin’s SDK runner does the same.
The plugin id is the runner’s first positional argument. The paths the
plugin writes to arrive on the ctx object (see
State and storage).
IPC envelope
The plugin host listens on the socket before the plugin starts. The plugin connects on startup and speaks length-prefixed msgpack frames: a 4-byte big-endian length, then the msgpack-encoded envelope. Frames over 4 MiB are refused.capability field on the wire. It resolves the
required capability from the method name on every request, then checks the
granted set carried by the token. Forging the field fails with a
capability-denied error.
The plugin class and lifecycle hooks
A plugin’s agent half is a class named by the manifestentrypoint
(module:Class, or a Plugin class in a file-path entrypoint). The runner
imports it, constructs it, and calls async lifecycle hooks if they are
defined. The hooks are duck-typed; implement the ones you need:
on_install, on_enable, on_configure (with
the static config) and then on_start, each only if present. on_stop and
on_disable run when the unit receives SIGTERM. See
Lifecycle for the full order.
ctx is a PluginContext built by the runner. It exposes the
capability-gated facades ctx.events, ctx.mavlink, ctx.msp,
ctx.peripheral_manager, ctx.camera, ctx.video, ctx.flight,
ctx.buttons, ctx.vision, ctx.telemetry, ctx.config_kv (live
key-value config), ctx.process, ctx.lifecycle and ctx.tools, plus
ctx.config (the static config dict), ctx.plugin_id, ctx.agent_id,
ctx.data_dir, ctx.config_dir, ctx.temp_dir and ctx.log. You receive
it as an argument and never construct it; the reference plugins in
ADOSExtensions/extensions/ are working examples.
Capability enforcement
Capabilities are enforced in the plugin host, not in the plugin. Each method maps to a required capability; the host checks the token’s granted set before the handler runs and rejects an ungranted caller before any work happens. In the plugin, the call raisesCapabilityDenied:
ados plugin perms <id> --revoke <cap>. A grant or revoke re-renders the
unit and restarts the plugin, so both the wire checks and the sandbox follow
the new set. The capability list and risk levels live in
Permissions.
Sandbox and identity
Four mechanisms isolate a plugin beyond the wire checks.- Its own system user. Every plugin runs as
ados-plg-<8 hex>(the same hash as the unit name) with primary groupados-plugins. Distinct users stop one plugin from reading another’s/proc/<pid>/environ, tracing it, or reaching its socket. The user is created at install and removed with the plugin. - Grant-driven device policy. With no hardware capability granted the
unit gets
PrivateDevices=yes, a private/devholding only pseudo-devices. Each granted hardware capability (hardware.uart,hardware.i2c,hardware.spi,hardware.gpio,hardware.usb,hardware.usb.uvc,hardware.camera.csi) switches toDevicePolicy=closedwith exactly theDeviceAllow=lines it names. Ahardware.uartgrant still cannot open the flight controller’s configured serial port. - Network and filesystem. Without
network.outboundthe unit allows onlyAF_UNIXsockets and denies all IP addresses. No plugin binds an inet port unless a declared service holdsnetwork.listen, and then only its declared ports./run/adosis an empty read-only tmpfs with only the plugin’s own socket directory and the log socket bound back in, and the agent’s credential files under/etc/adosare inaccessible.filesystem.hostopens/srv,/mnt,/mediaand/boot. - Declared services and HTTP.
agent.contributes.servicesrender extra units that run as the plugin’s user under the same sandbox.agent.http: truegives the plugin an HTTP socket the agent proxies.shared_topicsandpayloadsare manifest-level declarations; see the Manifest reference and Contribution points.
cgroup limits
The unit carries resource caps from the manifest’sagent.resources block:
MemoryMax from max_ram_mb, CPUQuota from max_cpu_percent, and
TasksMax from max_pids. The kernel enforces them. A memory breach is
OOM-killed, a CPU breach throttles, and a tasks breach refuses new threads
or processes. The shared slice runs at IOWeight=10, so a plugin loses
every contended disk block to the flight services. Budgets and how to read
the live counters are on Performance and budgets.
Restart policy
There is no start-rate limit: a crashing plugin is retried every 2 s
indefinitely and never latches
failed, so a plugin whose host socket is
not up yet recovers on its own. A clean exit (status 0) is not restarted;
if your plugin should run indefinitely, do not return from on_start until
on_stop is called. Plugin units carry no watchdog.
Generated unit
Units are generated at install and re-rendered on every grant and revoke. This is the pinned unit for a Rust plugincom.example.rustplug
granted hardware.i2c, network.outbound and filesystem.host:
# ---- capability sandbox is re-rendered on every grant
or revoke. ExecStartPre=+... guard-load runs privileged before the plugin
and fails the start if the guard cannot load. NoExecPaths=/ with the
ExecPaths= allowlist means the plugin can execute only its own install
tree and the shared libraries, never /bin/sh.
A Python plugin differs in two lines: ExecStart=/opt/ados/venv/bin/ados-plugin-runner <id>,
and ExecPaths= also lists the runner’s virtualenv. The runner loads the
manifest, puts the plugin source on sys.path, imports the entrypoint
class, connects to the socket, and dispatches into your hooks. A
runtime: rust plugin’s binary is executed directly.
Updates and restarts
Plugins are not hot-reloaded. Installing a new version stops the old process, re-renders the unit, and starts fresh. The data directory survives; see Versioning and updates.Debugging tips
ados plugin logs <id> --followtails the plugin’s log.systemctl status 'ados-plugin-<sanitized id>-*'shows the live unit state of the main unit and its services.systemctl cat 'ados-plugin-<sanitized id>-*'prints the generated units.ados plugin info <id>prints granted permissions and install state.
Next steps
GCS plugin deep dive
How the Mission Control half runs, talks to the host and renders.
Performance and budgets
Resource caps, throttling, memory pressure and profiling.