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

# Python SDK

> ados.sdk reference. The driver, vision, and testing surface every agent-side Python plugin uses.

A plugin's agent half can be written in two languages. This page covers
the Python SDK, `ados.sdk`. The other agent-half language is Rust, with
the [`ados-sdk` crate](/developers/sdk-rust); both speak the same
length-prefixed msgpack wire and capability-token handshake to the
plugin host, so the choice is per plugin. Python is the right pick when
the plugin leans on the Python ecosystem (machine-learning inference,
scripting, fast hardware bring-up); Rust is the right pick for a tight,
low-overhead, long-running service.

A Python plugin's agent half runs as a subprocess under
`ados-supervisor`. The public author surface is the `ados.sdk`
package: the typed driver base classes, the vision-engine client
types, and an in-process test harness. Import from `ados.sdk`, not
from internal agent modules.

<Warning>
  The plugin lifecycle context (the `ctx` object passed to a plugin's
  hooks) is **not yet re-exported from `ados.sdk`**. The SDK's own
  docstring says the context, event, and MAVLink-component helpers
  "land here as the host APIs stabilise." The runtime hands `ctx` to the
  plugin at call time and the plugin uses it duck-typed: there is no
  imported context class to construct or subclass. A plugin that wants
  static typing declares a local `Protocol` for the parts of `ctx` it
  touches, the way the reference extensions do. This page documents the
  real, current surface and does not invent an API that does not exist
  yet.
</Warning>

## Install

The standalone `ados-sdk` PyPI package is still in flight. Until it
publishes, plugin source vendors the contract types directly from the
agent tree at
[`altnautica/ADOSDroneAgent/src/ados`](https://github.com/altnautica/ADOSDroneAgent/tree/main/src/ados),
or develops against a checked-out agent so `import ados.sdk` resolves.

## What `ados.sdk` exports today

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from ados.sdk import (
    # Driver base classes (one per hardware kind)
    CameraDriver, GimbalDriver, LidarDriver,
    GpsDriver, EscDriver, PayloadActuatorDriver,
    # Per-kind data types (candidate / capabilities / session / samples)
    CameraCandidate, CameraCapabilities, CameraSession, FrameBuffer,
    GimbalCandidate, GimbalCapabilities, GimbalSession, GimbalState,
    LidarCandidate, LidarCapabilities, LidarSession, LidarFrame, LidarPoint,
    GpsCandidate, GpsCapabilities, GpsSession, GpsFix,
    EscCandidate, EscCapabilities, EscSession, EscTelemetry,
    PayloadCandidate, PayloadCapabilities, PayloadSession,
    PayloadCommand, PayloadState,
    # Driver errors
    DriverError, DriverDeviceNotFound, DriverPermissionDenied,
    # Vision-engine client + types
    VisionClient, Frame, FrameDescriptor, FrameFormat, RingLayout,
    Detection, DetectionBatch, BoundingBox,
    ModelKind, ModelExecution, ModelMetadata, Pose, Odometry,
    # Testing
    PluginTestHarness, FakeVisionEngine, load_fixture,
)
```

Three groups:

* **Driver layer.** The base classes hardware-driver plugins
  subclass, plus the dataclasses they exchange with the host. See
  [driver layer](/developers/driver-layer) and the per-kind pages.
* **Vision.** `VisionClient` and the frame, detection, and model
  types a plugin uses to read frames from the vision engine and
  publish detections back.
* **Testing.** `PluginTestHarness`, `FakeVisionEngine`, and
  `load_fixture` let you exercise a plugin in-process with no real
  agent.

## The agent-half plugin shape

A plugin's agent half is a plain Python class. There is no base class
to import or subclass. The manifest's `agent.entrypoint` points the
runner at it, in one of two forms:

* `module:Class`, for code packaged as an importable module
  (`follow_me:FollowMePlugin`).
* A path inside the archive (`agent/plugin.py`), in which case the
  class must be named `Plugin`.

The runner constructs the class with no arguments, then calls the
lifecycle hooks it finds. Every hook is optional and may be sync or
async, and `ctx` is duck-typed (see the warning above):

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
class Plugin:
    async def on_start(self, ctx):
        # Build and register drivers, open subscriptions, etc.
        ...

    async def on_stop(self, ctx):
        # Release anything on_start acquired.
        ...

    async def on_disable(self, ctx):
        # Optional. Tear down when the plugin is disabled.
        ...
```

The full hook order the runner walks is `on_install`, `on_enable`,
`on_configure(ctx, config)`, `on_start`, then it waits for shutdown
(`SIGTERM`), then `on_stop`, `on_disable`. Most plugins implement
only `on_start` and `on_stop`.

`ctx` is the runtime-provided plugin context. It exposes the host
service helpers a plugin needs, including `ctx.mavlink.subscribe(...)`
and `ctx.mavlink.send(...)`, `ctx.events.subscribe(...)` and
`ctx.events.publish(...)`, `ctx.vision.subscribe_detections(...)` and
`ctx.vision.designate_track(...)`,
`ctx.peripheral_manager.register_*_driver(...)`, `ctx.config`, and
`ctx.process.spawn`. Each call is gated on a manifest permission;
calling a method whose capability was not granted is rejected. Because
the context type is not yet published in `ados.sdk`, the reference
extensions declare a local `Protocol` for the parts they use. Read the
[thermal camera](/developers/camera-driver) and
[gimbal](/developers/gimbal-driver) extensions for the current shape.

### A real skeleton

This is the Follow-Me reference plugin, trimmed to its lifecycle shape.
It is a plain class the runner instantiates with no arguments. `on_start`
opens the subscriptions the plugin needs (FC pose over MAVLink, the
operator's designate clicks over the event bus, the detection stream
from the vision engine) and launches a control loop; `on_stop` and
`on_disable` tear it down. Each subscription matches a manifest
permission (`mavlink.read`, `event.subscribe`,
`vision.detection.subscribe`, `vision.track.designate`).

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import asyncio
from typing import Any

from ados.sdk.vision import BoundingBox, DetectionBatch


class FollowMePlugin:
    """Lifecycle-hook plugin class the runner instantiates with no args."""

    def __init__(self) -> None:
        self._ctx: Any = None
        self._loop_task: asyncio.Task | None = None
        self._locked_id: int | None = None

    async def on_start(self, ctx: Any) -> None:
        self._ctx = ctx

        # FC pose over MAVLink.
        await ctx.mavlink.subscribe("ATTITUDE", self._on_attitude)
        await ctx.mavlink.subscribe(
            "GLOBAL_POSITION_INT", self._on_global_position
        )

        # The operator's designate click on the event bus.
        await ctx.events.subscribe("follow.designate", self._on_designate)

        # Detection stream from the configured camera.
        await ctx.vision.subscribe_detections(self._on_batch)

        self._loop_task = asyncio.create_task(self._follow_loop())
        ctx.log.info("follow_me_started")

    async def on_stop(self, ctx: Any) -> None:
        if self._loop_task is not None:
            self._loop_task.cancel()
        ctx.log.info("follow_me_stopped")

    async def on_disable(self, ctx: Any) -> None:
        if self._loop_task is not None:
            self._loop_task.cancel()
```

The complete plugin (image-to-world projection, the standoff-follow
loop, the lock-state safety gate) lives in
[`altnautica/ADOSExtensions`](https://github.com/altnautica/ADOSExtensions)
under `extensions/follow-me/`.

## IPC

The supervisor passes a Unix domain socket path in the
`ADOS_PLUGIN_SOCKET` environment variable, the capability token in
`ADOS_PLUGIN_TOKEN`, and the bound drone id in `ADOS_PLUGIN_AGENT_ID`.
The runtime opens the socket and speaks msgpack RPC over it:

```
plugin -> {"id": "...", "type": "request", "method": "event.publish", "args": {...}}
host   -> {"id": "...", "type": "response", "args": {...}}
host   -> {"id": "...", "type": "event",    "method": "telemetry.battery", "args": {...}}
```

The envelope and the capability gate match the GCS bridge. Every
privileged method is checked against the granted capability set
before it reaches a handler; a call that exercises an ungranted
capability is rejected with a `capability_denied` error naming the
missing capability.

## Resource limits

The supervisor enforces the resource block from the manifest:

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
agent:
  resources:
    max_ram_mb: 96               # default 96, range 8 to 4096
    max_cpu_percent: 25          # default 25, range 1 to 100
    max_pids: 12                 # default 12, range 1 to 256
```

On Linux these become cgroup v2 controllers in the plugin slice
(`ados-plugins.slice`). The plugin sees them as hard caps; an
out-of-memory kill is reported to the host as a `crashed` lifecycle
event and trips the circuit breaker.

## Testing

`ados.sdk` ships an in-process harness. It wires a real plugin
context to fake host stubs so a single `pytest` run drives the
plugin's lifecycle hooks with no agent and no hardware:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import pytest
from ados.sdk import PluginTestHarness, load_fixture


@pytest.mark.asyncio
async def test_plugin_publishes_on_low_battery():
    async with PluginTestHarness(
        plugin_id="com.example.hello",
        granted_capabilities={"event.publish", "telemetry.read"},
    ) as harness:
        plugin = Plugin()
        await plugin.on_start(harness.context)

        # Deliver an event as if a peer published it.
        await harness.publish_event("telemetry.battery", {"remaining_percent": 18})

        # Assert on what the plugin published back.
        events = harness.published_events()
        assert any(topic == "battery.low" for topic, _ in events)
```

The harness:

* exposes `harness.context`, the `PluginContext` to pass to the
  plugin's hooks;
* injects events with `await harness.publish_event(topic, payload)`
  and captures the plugin's output with `harness.published_events()`;
* replays recorded scenarios with
  `await harness.replay_fixture(name_or_path)` (load files with
  `load_fixture`);
* grants and revokes capabilities at runtime with `harness.grant(...)`
  and `harness.revoke(...)`.

For vision plugins, `FakeVisionEngine` stands in for the real engine
and lets a test push synthetic frames into a `VisionClient`.

## Where the rest of the contract lives

The agent host modules at
[`altnautica/ADOSDroneAgent/src/ados/plugins/`](https://github.com/altnautica/ADOSDroneAgent/tree/main/src/ados/plugins)
define the install and runtime machinery the SDK sits on:

* `manifest` (the Pydantic manifest schema)
* `signing` (Ed25519 verification + revocation list)
* `archive` (pack and unpack with traversal protection)
* `state` (persistent install and permission state)
* `supervisor` (lifecycle: install / enable / disable / remove)
* `runner` (the process entrypoint that loads a plugin and calls its
  hooks)

Watch the [GitHub releases](https://github.com/altnautica) for the
`ados-sdk` PyPI publish, which will re-export the contracts above and
add the context helpers once they stabilise.
