> ## Documentation Index
> Fetch the complete documentation index at: https://docs.altnautica.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent plugin deep dive

> Subprocess model, IPC envelopes, capability tokens, cgroup limits, supervisor restart policy.

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.)

```
ados-plugins.slice
  ├── ados-plugin-com-example-battery.service   (subprocess #1)
  ├── ados-plugin-com-example-thermal.service   (subprocess #2)
  └── ados-plugin-com-example-gimbal.service     (subprocess #3)
```

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:

| Variable               | Purpose                                                                         |
| ---------------------- | ------------------------------------------------------------------------------- |
| `ADOS_PLUGIN_SOCKET`   | Unix domain socket path for IPC to the supervisor.                              |
| `ADOS_PLUGIN_TOKEN`    | Capability token minted per start, delivered through a `0600` environment file. |
| `ADOS_PLUGIN_AGENT_ID` | The drone id this instance targets, for per-drone plugins (empty otherwise).    |

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.

```
+----------------------+----------------------+
| u32 length (big-end) | msgpack envelope     |
+----------------------+----------------------+
```

Decoded, an envelope carries these fields (the wire form uses compact
msgpack keys, and the GCS postMessage bridge carries the same logical
fields):

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
    "id": "r42",                # request id, plugin-generated
    "type": "request",          # request | response | event
    "method": "mavlink.send",
    "capability": "mavlink.write",
    "args": {...},
    "token": "...",             # capability token on requests
    "version": 1,
    "error": None,              # only on response
}
```

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:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
class MyPlugin:
    async def on_start(self, ctx) -> None: ...
    async def on_configure(self, ctx, config: dict) -> None: ...
    async def on_stop(self, ctx) -> None: ...
```

`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.

<Note>
  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.
</Note>

## 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:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
try:
    await ctx.mavlink.send(frame_bytes)
except CapabilityDenied:
    # the operator did not grant mavlink.write
    ...
```

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:

```ini theme={"theme":{"light":"github-light","dark":"github-dark"}}
[Service]
Slice=ados-plugins.slice
MemoryMax=64M
CPUQuota=10%
TasksMax=4
```

`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:

```sh theme={"theme":{"light":"github-light","dark":"github-dark"}}
systemctl status ados-plugin-<id>.service
```

## Supervisor restart policy

The generated unit runs `Restart=on-failure` with a fixed `RestartSec`
of 2 seconds and a start-rate limit:

| Field                | Value        |
| -------------------- | ------------ |
| `Restart`            | `on-failure` |
| `RestartSec`         | `2s`         |
| `StartLimitInterval` | `60s`        |
| `StartLimitBurst`    | `5`          |

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:

```ini theme={"theme":{"light":"github-light","dark":"github-dark"}}
[Unit]
Description=ADOS plugin com.example.battery
After=ados-supervisor.service
PartOf=ados-supervisor.service

[Service]
Slice=ados-plugins.slice
Type=simple
Environment=ADOS_PLUGIN_SOCKET=/run/ados/plugins/com.example.battery.sock
EnvironmentFile=-/run/ados/plugins/com.example.battery.token.env
ExecStart=/opt/ados/venv/bin/ados-plugin-runner com.example.battery
Restart=on-failure
RestartSec=2s
StartLimitInterval=60s
StartLimitBurst=5
MemoryMax=64M
CPUQuota=10%
TasksMax=4
StandardOutput=append:/var/log/ados/plugins/com-example-battery.log
StandardError=append:/var/log/ados/plugins/com-example-battery.log
User=ados
Group=ados
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/ados/plugin-data /var/log/ados/plugins /run/ados/plugins
LockPersonality=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes

[Install]
WantedBy=ados-supervisor.service
```

`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.

<Tip>
  For the full symptom-to-fix list (install failures, capability denials,
  a plugin stuck in `failed`, IPC handshake errors), see
  [Troubleshooting](/developers/troubleshooting).
</Tip>

## See also

* [Manifest reference](/developers/manifest)
* [Permissions](/developers/permissions)
* [Performance and budgets](/developers/performance-and-budgets)
