Skip to main content
This page is for plugin authors writing non-trivial agent halves. It walks the runtime path top to bottom: how the host spawns your code, how IPC frames travel, how capabilities gate every privileged call, and how the supervisor decides when your plugin has misbehaved.

Subprocess model

Every third-party agent plugin runs as its own process, started by systemd as a generated unit inside the shared ados-plugins.slice cgroup slice. There is no in-process plugin tier for third parties; isolation is mandatory. (Built-in plugins that declare isolation: inprocess import into the supervisor and skip all of this.)
The unit name is derived from the plugin id with dots replaced by hyphens, so com.example.battery becomes ados-plugin-com-example-battery.service. Each subprocess receives a small fixed environment: The plugin id is passed as the runner’s positional argument, not an env var. Paths the plugin writes to (ctx.data_dir, ctx.config_dir, ctx.temp_dir) are resolved by the runner and handed to the plugin on the ctx object, not through the environment.

IPC envelope

The supervisor opens the Unix socket before spawning the plugin and listens on it. The plugin connects on startup and speaks length-prefixed msgpack frames: a 4-byte big-endian length, then the msgpack-encoded envelope.
Decoded, an envelope carries these fields (the wire form uses compact msgpack keys, and the GCS postMessage bridge carries the same logical fields):
The supervisor never trusts the capability field on the wire. It re-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 manifest entrypoint (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_start runs once after the IPC handshake. on_configure runs with the plugin’s config. on_stop runs when the supervisor sends SIGTERM. on_install, on_enable, and on_disable are also called if present. The ctx is a PluginContext with capability-gated facades: ctx.events, ctx.mavlink, ctx.peripheral_manager (alias ctx.peripherals), ctx.camera, ctx.vision, ctx.telemetry, ctx.config, and ctx.process. These are the host surfaces; the IPC client underneath is an implementation detail.
The author SDK package ados.sdk currently exports the typed driver base classes, the vision types, and the test harness. The PluginContext facade surface is host-side and not yet re-exported from ados.sdk; the reference plugins in ADOSExtensions/extensions/ are the working examples to copy from.

Capability enforcement

Capabilities are enforced on the supervisor side, not in the plugin. Each method maps to a required capability; the supervisor checks the token’s granted set before the handler runs and rejects an ungranted caller before any work happens. From the plugin’s perspective, a call that needs a capability you were not granted raises a CapabilityDenied error:
Operators can revoke a capability at any time from the GCS or with ados plugin perms <id> --revoke <cap>. The plugin loses access on the next token rotation, and matching calls then fail with the same denial.

cgroup limits

Every plugin runs inside the ados-plugins.slice slice, and the generated unit carries resource caps drawn from the manifest’s agent.resources block:
MemoryMax comes from max_ram_mb, CPUQuota from max_cpu_percent, and TasksMax from max_pids. The kernel enforces the caps. A memory breach is OOM-killed; a CPU breach throttles rather than kills; a tasks breach refuses new threads or processes. Read live numbers with:

Supervisor restart policy

The generated unit runs Restart=on-failure with a fixed RestartSec of 2 seconds and a start-rate limit: Five failed starts inside a 60-second window trip the limit: systemd stops trying and the supervisor records the plugin as failed. The operator sees the failure in the GCS event stream and decides whether to disable, remove, or investigate. A clean exit (return from on_start with no error) is treated as “the plugin is done” and is not restarted; if your plugin should run indefinitely, do not return from on_start until on_stop is called.

Service unit generation

Service units are generated from the manifest at install time and written to /etc/systemd/system/ados-plugin-<id>.service. A typical Python-plugin unit:
ados-plugin-runner is the SDK’s process entrypoint. It loads the plugin’s manifest, puts the unpacked plugin source on sys.path, imports the entrypoint class, opens the IPC socket, and dispatches into your lifecycle hooks. A plugin that declares runtime: rust ships its own compiled binary instead; the unit execs that binary directly and the Python runner is not involved.

Hot reload

Plugins are not hot-reloaded across version bumps. Updating from v1.0 to v1.1 stops the old subprocess, regenerates the unit, and starts fresh. State on disk under the plugin’s data dir survives.

Debugging tips

  • ados plugin logs <id> --follow tails the plugin’s log file at /var/log/ados/plugins/<id>.log.
  • systemctl status ados-plugin-<id>.service shows the live unit state.
  • systemctl cat ados-plugin-<id>.service shows the generated unit.
  • ados plugin info <id> prints granted permissions and install state.
For the full symptom-to-fix list (install failures, capability denials, a plugin stuck in failed, IPC handshake errors), see Troubleshooting.

See also