Skip to main content
A plugin can ship up to three pieces in one archive:
  • An agent half (a subprocess on the drone, written in Python or Rust).
  • A GCS half (TypeScript bundle in a sandboxed iframe).
  • One or more drivers registered by the agent half.
Each half has its own runtime, its own SDK, and its own sandbox. What makes them one plugin is the shared manifest, shared id, shared version, and shared install state. The agent half declares its language with agent.runtime: python (the default) or agent.runtime: rust. A Python half uses the ados.sdk package; a Rust half links the ados-sdk crate. Both speak the same length-prefixed msgpack wire to the agent’s plugin host, so a Rust agent half and a Python or Rust host interoperate without changes. The GCS half uses the @altnautica/plugin-sdk TypeScript SDK regardless of which language the agent half is written in.

When to ship two halves

Ship two halves when the plugin needs both of:
  • Hardware or low-level system access on the drone.
  • An operator-facing UI in the GCS.
The Battery Health plugin is GCS-only because it reads normalized telemetry and renders a chart. The Thermal Camera plugin is hybrid because it needs hardware.usb.uvc on the drone (driver half) plus a video overlay in the GCS. Do not split work that belongs in one half. If the plugin only needs telemetry and renders a panel, it is GCS-only. If the plugin only does background analytics with no operator UI, it is agent-only.

Lifecycle ordering

Install order is fixed:
  1. Operator drops .adosplug into the GCS install dialog.
  2. Manifest is parsed (no disk writes).
  3. Operator approves permissions. Both halves’ permissions appear in one grid.
  4. Host calls install on both halves in parallel. The agent host unpacks the agent half; the GCS host registers the bundle in the user’s install record.
  5. On enable, the agent half starts first as its own systemd subprocess (the Python interpreter for a runtime: python half, or the cross-compiled binary for a runtime: rust half).
  6. Once the agent half is running, the GCS half mounts in its iframe. The GCS bridge gates outbound RPC to the agent until the agent half is listening.
Disable runs in reverse: GCS unmounts first, then the supervisor sends SIGTERM to the agent.

Cross-half communication

The two halves do not speak directly. They speak through the host’s event bus.
Use the plugin’s own plugin.<id>.* topics. The agent half publishes on the full topic, prefixed with plugin. and the plugin id:
A topic that starts with plugin.<your-id>. is always yours to publish and to subscribe to. Any other topic falls under the event.publish and event.subscribe capabilities and is checked against the allowlist: reserved namespaces (vehicle., mavlink., mission., safety., agent., swarm., gps.) cannot be published into, and a small set of public safety and lifecycle topics (vehicle.armed, mission.started, agent.ready, and similar) can be subscribed to without a per-topic allowlist entry. The GCS half receives host-pushed events through ctx.events.subscribe, so it does not open a network connection of its own.

Shared manifest

One manifest. Two contributing blocks:
The top-level fields (id, name, version, license, risk) are flat, not nested under a plugin: block. Permissions are a list of - id: <capability> entries. Both halves carry the same id and version, so the host treats them as one install row.

Version compatibility between halves

Both halves ship in the same archive, so they always carry the same version. The supervisor refuses to load mismatched halves; that case can only happen if someone unpacks an archive and hand-edits one side. What can drift across versions:
  • Topic schemas published from the agent and consumed by the GCS. Use a schema_version field in your published payload and branch on it in the GCS.
  • Persistent state on disk written by the agent and read by the GCS via Convex. Carry a data_version in those rows.
The SDK does not inspect topic payload schemas; it is your job to keep them backward compatible inside a major version.

Driver halves

Drivers live inside the agent half. In Python you implement one of the typed driver base classes (CameraDriver, GimbalDriver, LidarDriver, GpsDriver, EscDriver, PayloadActuatorDriver); in Rust you implement the matching driver trait. Either way you register the instance with the peripheral manager. See driver layer for the contract. A plugin can ship multiple drivers. A thermal camera plugin registers a CameraDriver for the sensor. A multi-sensor plugin can register a CameraDriver plus a LidarDriver from the same agent half:
Each registration is gated by its own capability (sensor.camera.register, sensor.lidar.register). Both must be declared in the manifest.

Worked example: hybrid plugin skeleton

Agent entry point. For a file-path entrypoint (agent/plugin.py), the runner imports a class named Plugin and calls its lifecycle hooks:
GCS entry point. The manifest declares which panel renders in which slot; the bundle sets up its UI from the single mount call:

Failure modes

See also